溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

微信公眾號開發(fā)者模式介紹及接入

發(fā)布時間:2020-09-25 13:57:10 來源:網(wǎng)絡(luò) 閱讀:7341 作者:ZeroOne01 欄目:移動開發(fā)

Java公眾號開發(fā)環(huán)境搭建

需要準(zhǔn)備的東西:

  • 一個微信公眾號,參考:微信公眾號申請及介紹
  • 內(nèi)網(wǎng)穿透工具,參考:使用natapp開啟內(nèi)網(wǎng)穿透之旅

數(shù)據(jù)交互

編輯模式和開發(fā)模式的關(guān)系:
微信公眾號開發(fā)者模式介紹及接入

編輯模式和開發(fā)模式是互斥的關(guān)系,也就是說,當(dāng)我們使用開發(fā)模式時,編輯模式下的操作就會失效。反之,使用編輯模式時,開發(fā)模式下的操作就會失效,所以只能使用其中一個模式進(jìn)行公眾號的開發(fā)。

開發(fā)模式下,公眾號數(shù)據(jù)的交互流程:
微信公眾號開發(fā)者模式介紹及接入

注:圖中的微信公眾號服務(wù)器,就是我們開發(fā)者所要開發(fā)的部分


開發(fā)者模式接入

微信公眾平臺相關(guān)技術(shù)文檔地址如下:

  • 微信公眾平臺技術(shù)文檔
  • 入門指引
  • 接入指南

我們根據(jù) “接入指南” 中的說明來完成公眾平臺的接入,但是我們跳過文檔中的第一步,先來完成第二步的操作,即驗(yàn)證消息的確來自微信服務(wù)器。因?yàn)樘峤环?wù)器配置信息時微信會對配置的URL發(fā)起調(diào)用,驗(yàn)證該服務(wù)器是否正??捎?,所以我們得先把第二步完成,才能去完成第一步。既然是開發(fā)就得建工程了,所以在IDEA中創(chuàng)建一個SpringBoot工程,工程結(jié)構(gòu)如下:
微信公眾號開發(fā)者模式介紹及接入

先說明一點(diǎn):當(dāng)我們提交服務(wù)器配置信息后,微信服務(wù)器將發(fā)送GET請求到填寫的服務(wù)器地址URL上,GET請求攜帶參數(shù)分別為signature、timestamp、nonce、echostr。開發(fā)者通過檢驗(yàn)signature對請求進(jìn)行校驗(yàn),若確認(rèn)此次GET請求來自微信服務(wù)器,則原樣返回echostr參數(shù)內(nèi)容,表示接入生效,成為開發(fā)者成功,否則接入失敗。加密/校驗(yàn)流程如下:

1)將token、timestamp、nonce三個參數(shù)進(jìn)行字典序排序
2)將三個參數(shù)字符串拼接成一個字符串進(jìn)行SHA1加密
3)開發(fā)者獲得加密后的字符串可與signature對比,標(biāo)識該請求來源于微信

可以看到,第二步中,我們需要將三個參數(shù)字符串拼接成一個字符串進(jìn)行SHA1加密,這就涉及到SHA1加密算法。那么就需要一個專門的工具類來完成SHA1加密,所以需要在util包中,新建一個 SHA1Util 類,用于進(jìn)行SHA1加密,代碼如下:

package org.zero01.weixin.mqdemo.util;

import java.security.MessageDigest;

/**
 * @program: mq-demo
 * @description: SHA1加密
 * @author: 01
 * @create: 2018-06-23 18:06
 **/
public final class SHA1Util {

    private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5',
            '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    /**
     * Takes the raw bytes from the digest and formats them correct.
     *
     * @param bytes the raw bytes from the digest.
     * @return the formatted bytes.
     */
    private static String getFormattedText(byte[] bytes) {
        int len = bytes.length;
        StringBuilder buf = new StringBuilder(len * 2);
        // 把密文轉(zhuǎn)換成十六進(jìn)制的字符串形式
        for (byte aByte : bytes) {
            buf.append(HEX_DIGITS[(aByte >> 4) & 0x0f]);
            buf.append(HEX_DIGITS[aByte & 0x0f]);
        }
        return buf.toString();
    }

    public static String encode(String str) {
        if (str == null) {
            return null;
        }
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
            messageDigest.update(str.getBytes());
            return getFormattedText(messageDigest.digest());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

在util包中,再新建一個 WechatMqCheckedUtil 工具類,用于校驗(yàn)微信發(fā)起調(diào)用時所傳遞的參數(shù),代碼如下:

package org.zero01.weixin.mqdemo.util;

import java.util.Arrays;

/**
 * @program: mq-demo
 * @description: 校驗(yàn)微信發(fā)起調(diào)用時所傳遞的參數(shù)
 * @author: 01
 * @create: 2018-06-23 17:57
 **/
public class WechatMqCheckedUtil {

    // 在公眾平臺上配置的自定義token
    private static final String token = "zeroJun";

    /**
     * 校驗(yàn)微信加密簽名
     *
     * @param signature 微信加密簽名
     * @param timestamp 時間戳
     * @param nonce     隨機(jī)字符串
     * @return
     */
    public static boolean checkedSignature(String signature, String timestamp, String nonce) {

        // 1.加入token進(jìn)行排序
        String[] paramArr = new String[]{token, timestamp, nonce};
        Arrays.sort(paramArr);

        // 2.拼接成字符串,進(jìn)行sha1加密
        StringBuilder content = new StringBuilder();
        for (String aParamArr : paramArr) {
            content.append(aParamArr);
        }

        String temp = SHA1Util.encode(content.toString());

        // 3.與signature參數(shù)進(jìn)行對比,并返回對比結(jié)果
        return temp.equals(signature);
    }
}

在controller包中,新建一個 WeChatMqController 控制器類,提供給微信調(diào)用的接口,代碼如下:

package org.zero01.weixin.mqdemo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.zero01.weixin.mqdemo.util.WechatMqCheckedUtil;

/**
 * @program: mq-demo
 * @description: 接入微信公眾平臺
 * @author: 01
 * @create: 2018-06-23 17:51
 **/
@RestController
@RequestMapping("/wechat/mq")
public class WeChatMqController {

    /**
     * 驗(yàn)證消息的確來自微信服務(wù)器
     *
     * @param signature 微信加密簽名
     * @param timestamp 時間戳
     * @param nonce     隨機(jī)數(shù)
     * @param echostr   隨機(jī)字符串
     * @return
     */
    @GetMapping("/common")
    public String token(@RequestParam("signature") String signature,
                        @RequestParam("timestamp") String timestamp,
                        @RequestParam("nonce") String nonce,
                        @RequestParam("echostr") String echostr) {

        // 驗(yàn)證成功則返回echostr
        if (WechatMqCheckedUtil.checkedSignature(signature, timestamp, nonce)) {
            System.out.println(echostr);
            return echostr;
        }
        return null;
    }
}

完成代碼的編寫,并運(yùn)行了工程及natapp客戶端后,就可以到公眾平臺上填寫服務(wù)器的配置信息了。進(jìn)入“基本配置” 的頁面,點(diǎn)擊 “修改配置” ,如下:
微信公眾號開發(fā)者模式介紹及接入

填寫好基本的配置:
微信公眾號開發(fā)者模式介紹及接入

提交配置:
微信公眾號開發(fā)者模式介紹及接入

提交成功后,啟用服務(wù)器配置:
微信公眾號開發(fā)者模式介紹及接入
微信公眾號開發(fā)者模式介紹及接入

到此為止,我們的開發(fā)者模式就接入完成了。此時,在編輯模式的界面中,可以看到編輯模式下的操作都已失效:
微信公眾號開發(fā)者模式介紹及接入


消息的接收與響應(yīng)

消息管理相關(guān)的文檔:

  • 接收普通消息
  • 接收事件推送

我們先來完成文本消息的接收及回復(fù),由于微信傳遞的數(shù)據(jù)是xml格式的,所以我們需要添加一些用于解析xml的包,在pom.xml中添加如下依賴:

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream -->
<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.3.1</version>
</dependency>

在工程中,新建一個vo包,在該包下新建一個 AllMessage 類,用于封裝所有普通消息的字段。關(guān)于不同類型的普通消息所包含的具體字段及描述信息,請參考:接收普通消息。代碼如下:

package org.zero01.weixin.mqdemo.vo;

import lombok.Data;

/**
 * @program: mq-demo
 * @description: 所有類型的消息封裝對象
 * @author: 01
 * @create: 2018-06-23 21:16
 **/
@Data // lombok注解
public class AllMessage {

    /**
     * 屬性名首字母大寫的原因是因?yàn)榉祷氐膞ml中標(biāo)簽的名稱是需要大寫的,否則微信解析不了
     */
    private String ToUserName;  // 接收方賬號
    private String FromUserName;  // 發(fā)送方賬號
    private long CreateTime;  // 消息創(chuàng)建時間 (整型)
    private String MsgType;  // 消息類型
    private String PicUrl;  // 消息內(nèi)容
    private String Content;  // 消息內(nèi)容
    private String MediaId;  // 消息媒體id,可以調(diào)用多媒體文件下載接口拉取數(shù)據(jù)。
    private String Format;  // 語音格式,如amr,speex等
    private String Recognition;  // 語音識別結(jié)果,UTF8編碼
    private String MsgId;  // 消息id,64位整型
    private String ThumbMediaId;  // 視頻消息縮略圖的媒體id,可以調(diào)用多媒體文件下載接口拉取數(shù)據(jù)。
    private String Location_X;  // 地理位置維度
    private String Location_Y;  // 地理位置經(jīng)度
    private String Scale;  // 地圖縮放大小
    private String Label;  // 地理位置信息
    private String Title;  // 消息標(biāo)題
    private String Description;  // 消息描述
    private String Url;  // 消息鏈接
    private String Event;  // 事件類型
    private String EventKey; // 事件KEY值
    private String Ticket;  // 二維碼的ticket
        private String MenuId; // 指菜單ID,如果是個性化菜單,則可以通過這個字段,知道是哪個規(guī)則的菜單被點(diǎn)擊了
    private ScanCodeInfo ScanCodeInfo;

    // 掃描信息
    public static class ScanCodeInfo {
        private String ScanType;  // 掃描類型,一般是qrcode
        private String ScanResult;  // 掃描結(jié)果,即二維碼對應(yīng)的字符串信息
    }
}

新建一個common包,并在該包中,新建一個 MessageTypeEnum 枚舉類,用于存放普通消息的類型。代碼如下:

package org.zero01.weixin.mqdemo.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @program: mq-demo
 * @description: 普通消息類型
 * @author: 01
 * @create: 2018-06-24 14:00
 **/
@Getter
@AllArgsConstructor
public enum MessageTypeEnum {

    MSG_TEXT("text"),  // 文本消息類型
    MSG_IMAGE("image"), // 圖片消息類型
    MSG_VOICE("voice"), // 語音消息類型
    MSG_VIDEO("video"), // 視頻消息類型
    MSG_SHORTVIDEO("shortvideo"), // 小視頻消息類型
    MSG_LOCATION("location"), // 地理位置消息類型
    MSG_LINK("link"), // 鏈接消息類型
    MSG_EVENT("event"), // 事件消息類型
    ;

    private String msgType;
}

事件消息類型中包含訂閱/取消訂閱兩種事件類型,所以我們也需要增加一個枚舉來存放這兩種事件類型。代碼如下:

package org.zero01.weixin.mqdemo.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @program: mq-demo
 * @description: 事件推送類型
 * @author: 01
 * @create: 2018-06-24 14:09
 **/
@Getter
@AllArgsConstructor
public enum EventType {

    EVENT_SUBSCRIBE("subscribe"),  // 訂閱事件類型
    EVENT_UNSUBSCRIBE("unsubscribe"),  // 取消訂閱事件類型
    ;

    private String eventType;
}

我們希望有一個專門的地方,來配置我們的自動回復(fù)內(nèi)容,所以再次新建一個枚舉類,用于存放自動回復(fù)的內(nèi)容。代碼如下:

package org.zero01.weixin.mqdemo.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @program: mq-demo
 * @description: 回復(fù)的內(nèi)容
 * @author: 01
 * @create: 2018-06-24 14:09
 **/
@AllArgsConstructor
@Getter
public enum ContentEnum {
    CONTENT_SUBSCRIBE("你好,歡迎關(guān)注zero菌~"),
    CONTENT_NONSUPPORT("暫不支持文本以外的消息回復(fù)!"),
    CONTENT_PREFIX("你發(fā)送的消息是:"),
    ;
    private String content;
}

在util包下,新建一個 MessageUtil 工具類,用于轉(zhuǎn)換消息數(shù)據(jù)類型,代碼如下:

package org.zero01.weixin.mqdemo.util;

import com.thoughtworks.xstream.XStream;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.zero01.weixin.mqdemo.vo.AllMessage;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @program: mq-demo
 * @description: 轉(zhuǎn)換消息數(shù)據(jù)類型的工具類
 * @author: 01
 * @create: 2018-06-23 21:04
 **/
public class MessageUtil {

    private final static String XML = "xml";

    /**
     * xml轉(zhuǎn)換為map集合
     *
     * @param request
     * @return
     * @throws IOException
     * @throws DocumentException
     */
    public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
        Map<String, String> map = new HashMap<>();
        SAXReader reader = new SAXReader();

        InputStream inputStream = request.getInputStream();
        Document document = reader.read(inputStream);

        Element root = document.getRootElement();

        List<Element> elementList = root.elements();

        for (Element element : elementList) {
            map.put(element.getName(), element.getText());
        }
        inputStream.close();

        return map;
    }

    /**
     * 將 AllMessage 消息對象,轉(zhuǎn)換為xml
     *
     * @param allMessage
     * @return
     */
    public static String allMessageToXml(AllMessage allMessage) {
        XStream xStream = new XStream();
        xStream.alias(XML, allMessage.getClass());

        return xStream.toXML(allMessage);
    }

    /**
     * 將 AllMessage 消息對象,轉(zhuǎn)換為xml,并指定content的內(nèi)容
     *
     * @param allMessage
     * @return
     */
    public static String allMessageToXml(AllMessage allMessage, String content) {
        allMessage.setContent(content);

        return allMessageToXml(allMessage);
    }

    /**
     * 將xml轉(zhuǎn)換為 AllMessage消息對象
     *
     * @param xmlStr
     * @return
     */
    public static AllMessage xmlToAllMessage(String xmlStr) {
        XStream xStream = new XStream();
        AllMessage allMessage = new AllMessage();
        xStream.aliasType(XML, allMessage.getClass());
        allMessage = (AllMessage) xStream.fromXML(xmlStr);

        return allMessage;
    }

    /**
     * 將xml轉(zhuǎn)換為 AllMessage 消息對象,并指定content的內(nèi)容
     *
     * @param xmlStr
     * @param content
     * @return
     */
    public static AllMessage xmlToAllMessage(String xmlStr, String content) {
        AllMessage allMessage = xmlToAllMessage(xmlStr);
        allMessage.setContent(content);

        return allMessage;
    }

    /**
     * 設(shè)置并獲取文本消息類型的 AllMessage 對象
     * @param fromUserName
     * @param toUserName
     * @param content
     * @return
     */
    public static AllMessage setGetTextMsg(String fromUserName, String toUserName, String content) {
        AllMessage allMessage = new AllMessage();
        allMessage.setFromUserName(toUserName);
        allMessage.setToUserName(fromUserName);
        allMessage.setMsgType(MessageTypeEnum.MSG_TEXT.getMsgType());
        allMessage.setCreateTime(new Date().getTime());
        allMessage.setContent(content);

        return allMessage;
    }

    /**
     * 自動回復(fù)
     * @param allMessage
     * @param content
     * @return
     */
    public static String autoReply(AllMessage allMessage,String content) {
        allMessage = setGetTextMsg(allMessage.getFromUserName(), allMessage.getToUserName(), content);

        return allMessageToXml(allMessage);
    }
}

最后在 WeChatMqController 控制器類中,新增接收微信公眾號消息的接口。注意,接口映射的uri也是/wechat/mq/common,但請求方式是post。代碼如下:

/**
 * 接收微信公眾號消息的接口
 *
 * @param xmlStr
 * @return
 */
@PostMapping("/common")
public String text(@RequestBody String xmlStr) {
    // 將xml格式的數(shù)據(jù),轉(zhuǎn)換為 AllMessage 對象
    AllMessage allMessage = MessageUtil.xmlToAllMessage(xmlStr);

    // 是否是文本消息類型
    if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_TEXT.getMsgType())) {
        // 自動回復(fù)用戶所發(fā)送的文本消息
        return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_PREFIX.getContent() + allMessage.getContent());
    }
    // 是否是事件推送類型
    else if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_EVENT.getMsgType())) {
        // 是否為訂閱事件,即公眾號被關(guān)注時所觸發(fā)的事件
        if (EventType.EVENT_SUBSCRIBE.getEventType().equals(allMessage.getEvent())) {
            // 自動回復(fù)歡迎語
            return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_SUBSCRIBE.getContent());
        }
    } else {
        // 暫不支持文本以外的消息回復(fù)
        return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
    }
    return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
}

編寫完以上代碼后,運(yùn)行SpringBoot工程以及natapp客戶端,接著向公眾號發(fā)送各種類型的普通消息,自動回復(fù)結(jié)果如下:
微信公眾號開發(fā)者模式介紹及接入

如上圖,可以看到,當(dāng)公眾號被關(guān)注時,回復(fù)了歡迎語。并成功接收了所有類型的普通消息,進(jìn)行了相應(yīng)的自動回復(fù)。到此為止,我們就完成了公眾號開發(fā)模式的接入。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI