微信公眾號(hào)

微信公眾號(hào)開發(fā)
1绣张、公眾號(hào)機(jī)器人:包括設(shè)置菜單、自動(dòng)回復(fù)关带、推送消息
2、公眾號(hào)網(wǎng)頁(yè):即在網(wǎng)頁(yè)中調(diào)用微信的JS-SDK沼撕;網(wǎng)頁(yè)必須從屬于公眾號(hào)(服務(wù)器端給前端提供授權(quán)信息)宋雏,才能得到調(diào)用JS-SDK的授權(quán)

公眾號(hào)機(jī)器人 和 公眾號(hào)網(wǎng)頁(yè) 都要用到的 access_token 管理

用Bean(全局可用的單例)來管理access_token
注意:在不同機(jī)子上獲取 access_token,會(huì)使得其他機(jī)子上的access_token失效
1务豺、service

@Service
public class WeixinService {
    @Value("${wx.appId}")
    private String appId;

    @Value("${wx.appSecret}")
    private String appSecret;

    @Autowired
    private RestTemplate restTemplate;

    private String accessToken;

    private String jsApiTicket;      // 只在 公眾號(hào)網(wǎng)頁(yè)用到

    private Date tokenExpire;

    private Date ticketExpire;

    public String fetchToken() {
        String urlStr = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
        WxSession wxSession = restTemplate.getForObject(urlStr, WxSession.class);
        accessToken = wxSession.getAccess_token();
        long millis = System.currentTimeMillis() + (wxSession.getExpires_in() - 300) * 1000;
        tokenExpire = new Date(millis);
        return accessToken;
    }

    public String fetchTicket() {
        String accessToken = getAccessToken();
        String urlStr = String.format("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi", accessToken);
        WxSession wxSession = restTemplate.getForObject(urlStr, WxSession.class);
        jsApiTicket = wxSession.getTicket();
        long millis = System.currentTimeMillis() + (wxSession.getExpires_in() - 300) * 1000;
        ticketExpire = new Date(millis);
        return jsApiTicket;
    }

    public String getAccessToken() {
        if (accessToken == null || tokenExpire == null || tokenExpire.before(new Date())) {
            fetchToken();
        }
        return accessToken;
    }

    public String getJsApiTicket() {
        if (ticketExpire == null || ticketExpire.before(new Date())) {
            fetchTicket();
        }
        return jsApiTicket;
    }
}

2磨总、用于微信相關(guān)請(qǐng)求 和 響應(yīng) 的POJO

@Autowired
private WeixinService weixinService;

public class WxSession implements Serializable {
    private String access_token;    // 從微信獲取 access_token 的響應(yīng)
    private String ticket;   // 從微信獲取 jsapi_ticket 的響應(yīng)
    private String url;      // 前端請(qǐng)求 JS-SDK 簽名 的參數(shù)
    private long timestamp;    // 前端獲取 JS-SDK 簽名的響應(yīng)
    private String nonceStr;    // 前端獲取 JS-SDK 簽名的響應(yīng)
    private String signature;   // 前端獲取 JS-SDK 簽名的響應(yīng)
    private String appId;       // 前端獲取 JS-SDK 簽名的響應(yīng)
    private int expires_in;
    private int errcode = 0;
    private String errmsg;
}

3、由于 只有最后一次獲取的access_token 有效笼沥,因此將access_token放到一個(gè)redis里蚪燕;同時(shí)增加token無效時(shí)重試一次的機(jī)制

@Service
public class WeixinService {
    @Value("${wx.appId}")
    private String appId;

    @Value("${wx.appSecret}")
    private String appSecret;

    @Value("${wx.tokenUrl}")
    private String tokenUrl;

    @Value("${wx.subscribeUrl}")
    private String subscribeUrl;

    @Autowired
    private RestTemplate restTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private String accessToken;

    private Date tokenExpire;

    public String fetchToken() {
        String urlStr = String.format(tokenUrl, appId, appSecret);
        WxSession wxSession = restTemplate.getForObject(urlStr, WxSession.class);
        accessToken = wxSession.getAccess_token();
        long duration = wxSession.getExpires_in() - 300;
        tokenExpire = new Date(System.currentTimeMillis() + duration * 1000);
        stringRedisTemplate.opsForValue().set("access_token", accessToken);
        stringRedisTemplate.expire("access_token", duration, TimeUnit.SECONDS);
        return accessToken;
    }

    public String getAccessToken() {
        if (accessToken == null) {
            accessToken = stringRedisTemplate.opsForValue().get("access_token");
            if (accessToken != null) {
                Long millis = stringRedisTemplate.getExpire("access_token", TimeUnit.MILLISECONDS);
                if(millis != null){
                    tokenExpire = new Date(System.currentTimeMillis() + millis);
                }
            }
        }
        if (accessToken == null || tokenExpire == null || tokenExpire.before(new Date())) {
            fetchToken();
        }
        return accessToken;
    }

    // 推送訂閱時(shí)娶牌,如果access_token無效,再次獲取token馆纳,并重新推送一次
    public void subscribePost(Object request){
        String accessToken = this.getAccessToken();
        String urlStr = String.format(subscribeUrl, accessToken);
        WxSession wxSession = restTemplate.postForObject(urlStr, request, WxSession.class);
        if (wxSession.getErrcode() != 0) {
            if(wxSession.getErrcode() == 42001 || wxSession.getErrcode() == 40001){
                accessToken = this.fetchToken();
                urlStr = String.format(subscribeUrl, accessToken);
                restTemplate.postForObject(urlStr, request, WxSession.class);
            }
        }
    }
}

公眾號(hào)網(wǎng)頁(yè)

限制:
1诗良、【設(shè)置->公眾號(hào)設(shè)置->功能設(shè)置】JS接口安全域名 只能是已備案的域名,可以是http鲁驶,端口必須是80或443
2鉴裹、jsapi_ticket有效期兩個(gè)小時(shí),每次獲取都不一樣钥弯,只有最新的一次有效

獲取JS-SDK簽名

    @Value("${wx.appId}")
    private String appId;

    @Autowired
    private WeixinService weixinService;

    @RequestMapping(value = "/signature", method = RequestMethod.POST)
    public WxSession signature(@RequestBody WxSession wxSession) {
        String jsApiTicket = weixinService.getJsApiTicket();
        String nonceStr = RandomStringUtils.randomAlphanumeric(16);  // 隨機(jī)串
        long timestamp = System.currentTimeMillis() / 1000;  // 秒級(jí)時(shí)間戳
        String url = wxSession.getUrl();  // 前端傳來url
        /* 字段名 字典排序径荔,拼接 */
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("jsapi_ticket=" + jsApiTicket);
        stringBuffer.append("&noncestr=" + nonceStr);
        stringBuffer.append("&timestamp=" + timestamp);
        stringBuffer.append("&url=" + url);

        String signature = DigestUtils.sha1Hex(stringBuffer.toString());         // sha1 摘要
        return new WxSession(timestamp, nonceStr, signature, appId);
    }

公眾號(hào)機(jī)器人

限制:
1、被動(dòng)回復(fù)用戶消息:要求在5秒內(nèi)必須回復(fù)
2脆霎、客服消息: 可以在用戶操作后48小時(shí)內(nèi)回復(fù)
3总处、回復(fù)圖片或視頻:回復(fù)多媒體消息只能是已經(jīng)上傳到【微信公眾平臺(tái)】的素材
4、access_token有效期兩個(gè)小時(shí)睛蛛,每次獲取都不一樣鹦马,只有最新的一次有效
5、媒體文件(臨時(shí)素材)在微信后臺(tái)保存時(shí)間為3天玖院,即3天后media_id失效
6菠红、永久素材庫(kù)保存總數(shù)量有上限:圖文消息素材、圖片素材上限為100000难菌,其他類型為1000
7试溯、用戶能發(fā)送的媒體格式 mp4、jpg郊酒、jpeg遇绞、png
8、不管用戶發(fā)的圖片是jpg還是png燎窘,微信發(fā)給我方服務(wù)器的 PicUrl 的 Content-Type 都是 image/jpeg
9摹闽、【開發(fā)->基本設(shè)置->服務(wù)器配置】服務(wù)器地址(URL)可以是IP,可以是http褐健,端口必須是80或443

服務(wù)器配置

1付鹿、驗(yàn)證服務(wù)器(微信會(huì)發(fā)來多次請(qǐng)求,每次都響應(yīng)正確即可驗(yàn)證通過)

    @RequestMapping(value = "", method = RequestMethod.GET)
    public String verify(@RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr) {
        List<String> list = new ArrayList<String>();
        list.add(nonce);
        list.add(timestamp);
        list.add("token");
        Collections.sort(list);  // 將nonce蚜迅、timestamp舵匾、token進(jìn)行字典排序
        // 連接后進(jìn)行 sha1 摘要
        String computSignature = DigestUtils.sha1Hex(list.get(0) + list.get(1) + list.get(2));
        // 對(duì)比自己計(jì)算的signature 和 微信傳來的signature 
        if (computSignature.equals(signature)) {
            // 對(duì)比一致則返回 echostr 源串
            return echostr;
        }
        return "驗(yàn)證錯(cuò)誤!";
    }

開啟 服務(wù)器配置

1谁不、【微信公眾平臺(tái)】開啟 服務(wù)器配置 坐梯,會(huì)導(dǎo)致【微信公眾平臺(tái)】上設(shè)置的 自定義菜單和自動(dòng)回復(fù)失效
注:服務(wù)器配置開啟后,自定義菜單需要調(diào)用微信的接口進(jìn)行設(shè)置刹帕,調(diào)用微信接口都需要傳access_token吵血;
用戶發(fā)送的消息會(huì)以POST的方式發(fā)送到配置的服務(wù)器URL
2谎替、【微信公眾平臺(tái)】獲取AppSecret
3、【微信公眾平臺(tái)】設(shè)置獲取access_token 的 IP白名單
4蹋辅、【微信公眾平臺(tái)接口調(diào)試工具】發(fā)送"獲取Access Token"請(qǐng)求钱贯,得到access_token
注:access_token有效期兩個(gè)小時(shí)

設(shè)置自定義菜單

自定義菜單的功能有:訪問頁(yè)面模板、訪問已群發(fā)過的文章晕翠、訪問小程序喷舀、發(fā)送素材
1、獲取現(xiàn)有自定義菜單:【微信公眾平臺(tái)接口調(diào)試工具】發(fā)送"自定義菜單查詢"請(qǐng)求
2淋肾、獲取"頁(yè)面模板"的URL:【微信公眾平臺(tái)】上復(fù)制
3硫麻、獲取歷史文章的URL:【微信公眾平臺(tái)】首頁(yè),找到歷史群發(fā)文章樊卓,得到文章鏈接拿愧,去掉URL中尾部幾個(gè)參數(shù)
4、獲取素材的media_id:【Postman】發(fā)送 "獲取素材列表" 請(qǐng)求
5碌尔、更新自定義菜單:【微信公眾平臺(tái)接口調(diào)試工具】發(fā)送"自定義菜單創(chuàng)建"請(qǐng)求

案例:用戶發(fā)送視頻浇辜,立馬回復(fù)文字,再異步回復(fù)客服消息

用戶發(fā)送的視頻唾戚,視作一個(gè)臨時(shí)素材柳洋;臨時(shí)視頻素材最大10MB,支持MP4格式
微信發(fā)來的數(shù)據(jù)是xml格式叹坦,返回給微信的數(shù)據(jù)也要是xml格式
1熊镣、pom.xml

        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>

2、XML解析募书、生成 工具類

public class XMLUtil {
    public static WxMsg parseXml(String xml) throws UnsupportedEncodingException,DocumentException {
        logger.info(xml);
        SAXReader reader = new SAXReader();
        Document document = reader.read(new ByteArrayInputStream(xml.getBytes("UTF-8")));
        Element root = document.getRootElement();
        String fromUserName = root.selectSingleNode("/xml/FromUserName").getText();
        String toUserName = root.selectSingleNode("/xml/ToUserName").getText();
        String msgType = root.selectSingleNode("/xml/MsgType").getText();
        WxMsg wxMsg = new WxMsg(fromUserName, toUserName, msgType);
        if (msgType.equals("video")) {
            String mediaId = root.selectSingleNode("/xml/MediaId").getText();
            wxMsg.setMediaId(mediaId);
        }
        if (msgType.equals("image")) {
            String picUrl = root.selectSingleNode("/xml/PicUrl").getText();
            wxMsg.setPicUrl(picUrl);
        }
        // 訂閱绪囱、點(diǎn)擊菜單 等事件
        if (msgType.equals("event")) {
            String event = root.selectSingleNode("/xml/Event").getText();
            wxMsg.setEvent(event);
            // 點(diǎn)擊菜單事件
            if (event.equals("CLICK")) {
                String eventKey = root.selectSingleNode("/xml/EventKey").getText();
                wxMsg.setEventKey(eventKey);
            }
        }
        return wxMsg;
    }

    public static String generateXml(WxMsg wxMsg) {
        Document document = DocumentHelper.createDocument();
        Element root = document.getRootElement();
        root = document.addElement("xml");
        root.addElement("FromUserName").addText(wxMsg.getFromUserName());
        root.addElement("ToUserName").addText(wxMsg.getToUserName());
        root.addElement("CreateTime").addText("" + System.currentTimeMillis());
        root.addElement("MsgType").addText(wxMsg.getMsgType());
        root.addElement("Content").addText(wxMsg.getContent());
        return document.asXML();
    }
}

3、Controller

    @RequestMapping(value = "", method = RequestMethod.POST)
    public String autoReply(HttpServletRequest request) throws Exception {
        // 微信發(fā)來的數(shù)據(jù)是xml格式
        String xml = new String(ByteStreams.toByteArray(request.getInputStream()));
        // 解析XML
        WxMsg receiveMsg = XMLUtil.parseXml(xml);
        String msgType = receiveMsg.getMsgType();
        String openId = receiveMsg.getFromUserName();
        WxMsg responseMsg = new WxMsg(receiveMsg.getToUserName(), openId, "text");
        // 區(qū)分動(dòng)作類型
        if (msgType.equals("event") && receiveMsg.getEvent().equals("subscribe")) {  // 訂閱事件
            responseMsg.setContent("歡迎訂閱");
            return XMLUtil.generateXml(responseMsg);
        }
        // 點(diǎn)擊菜單
        if (msgType.equals("event") && receiveMsg.getEvent().equals("CLICK") && receiveMsg.getEventKey().equals("XX")) {
            responseMsg.setContent("您點(diǎn)擊了XX菜單");
            return XMLUtil.generateXml(responseMsg);
        }
        if (!msgType.equals("video")) {
            return "";
        }
        String mediaId = receiveMsg.getMediaId();

        /* 相關(guān)業(yè)務(wù)邏輯莹捡,例如記錄數(shù)據(jù)到數(shù)據(jù)庫(kù) */

        // 根據(jù)mediaId鬼吵,異步 從微信下載視頻
        asyncTasks.downloadFromWx(openId, mediaId);
        // 給微信返回的也是 xml 格式
        responseMsg.setContent("立即回復(fù)給用戶的文字");
        return XMLUtil.generateXml(responseMsg);
    }

4、用于接收微信消息 和 回復(fù)微信消息的 POJO

public class WxMsg implements Serializable {
    private String FromUserName;
    private String ToUserName;
    private String MsgType;
    private String mediaId;
    private String PicUrl;
    private String Event;
    private String EventKey;
    private String Content;
    private String CreateTime;
}

5篮赢、異步任務(wù) 或 定時(shí)任務(wù)

@Component
@EnableAsync
public class AsyncTasks {
    @Autowired
    private RestTemplate restTemplate;

    @Async
    public void downloadFromWx(String openId, String mediaId) throws Exception {
        String appId = "***";
        String appSecret = "***";
        // 下載視頻
        String urlStr = String.format("http://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s", wexinService.getAccessToken(), mediaId);
        URL url = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection)url.openConnection();
        conn.setConnectTimeout(100*1000);
        InputStream inputStream = conn.getInputStream();

        /* 相關(guān)業(yè)務(wù)邏輯齿椅,例如記錄數(shù)據(jù)到數(shù)據(jù)庫(kù) */
    }
}

6、發(fā)送客服消息

        // 構(gòu)造url
        String url = String.format("https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=%s", wexinService.getAccessToken());
        // 構(gòu)造請(qǐng)求
        String content = "這是客服消息";
        restTemplate.postForObject( url, new ServiceMsg(openId,"text",content), String.class );

7启泣、客服消息請(qǐng)求體 POJO

public class ServiceMsg implements Serializable {
    private String touser;
    private String msgtype;
    private Text text;

    public class Text implements Serializable {
        private String content;
    }
}

案例:用戶發(fā)送文字媒咳,自動(dòng)回復(fù)一條視頻

1、接收用戶消息种远,記下openId,并立即(5秒內(nèi))返回一個(gè) 提示消息
2顽耳、異步任務(wù)調(diào)用 "新增臨時(shí)素材"坠敷,得到media_id
3妙同、(48小時(shí)內(nèi))調(diào)用"客服接口-發(fā)消息",帶上openId 和 media_id

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末膝迎,一起剝皮案震驚了整個(gè)濱河市粥帚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌限次,老刑警劉巖芒涡,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異卖漫,居然都是意外死亡费尽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門羊始,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旱幼,“玉大人,你說我怎么就攤上這事突委“芈保” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵匀油,是天一觀的道長(zhǎng)缘缚。 經(jīng)常有香客問我,道長(zhǎng)敌蚜,這世上最難降的妖魔是什么桥滨? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮钝侠,結(jié)果婚禮上该园,老公的妹妹穿的比我還像新娘。我一直安慰自己帅韧,他們只是感情好里初,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著忽舟,像睡著了一般双妨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叮阅,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天腹纳,我揣著相機(jī)與錄音,去河邊找鬼基显。 笑死分歇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勒叠。 我是一名探鬼主播兜挨,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼膏孟,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了拌汇?” 一聲冷哼從身側(cè)響起柒桑,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎噪舀,沒想到半個(gè)月后魁淳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡与倡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年界逛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒸走。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡仇奶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出比驻,到底是詐尸還是另有隱情该溯,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布别惦,位于F島的核電站狈茉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏掸掸。R本人自食惡果不足惜氯庆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扰付。 院中可真熱鬧堤撵,春花似錦、人聲如沸羽莺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)盐固。三九已至荒给,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間刁卜,已是汗流浹背志电。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛔趴,地道東北人挑辆。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親之拨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子茉继,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355