微信公众号开发(关注、取消、发送模板消息)

468 阅读4分钟

前言

现在很多业务会基于微信公众号实现。笔者做这部分开发的时候,项目不允许再引入外部 jar 包,故做的相当蛋疼。这里是总结时写的 demo 节选。

如何将 xml 消息转换成 json 对象?

xstream

maven 依赖

        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.12</version>
        </dependency>

消息原型(xmlString)

<xml>
  <ToUserName><![CDATA[gh_d0c6b73cc08e]]></ToUserName>
  <FromUserName><![CDATA[oHOLJw-r6lBxSXU4pRDKpoDyqWI0]]></FromUserName>
  <CreateTime>1587459486</CreateTime>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[subscribe]]></Event>
  <EventKey><![CDATA[]]></EventKey>
</xml>

对应的 java 对象

@Data
@XStreamAlias("xml")
public class XmlMessage implements Serializable {

    @XStreamAlias("ToUserName")
    @XStreamConverter(XStreamCDataConverter.class)
    private String toUser;
    @XStreamAlias("FromUserName")
    @XStreamConverter(XStreamCDataConverter.class)
    private String fromUser;
    @XStreamAlias("CreateTime")
    private Long createTime;
    @XStreamAlias("MsgType")
    @XStreamConverter(XStreamCDataConverter.class)
    private String msgType;
    @XStreamAlias("Event")
    @XStreamConverter(XStreamCDataConverter.class)
    private String event;
    @XStreamAlias("EventKey")
    @XStreamConverter(XStreamCDataConverter.class)
    private String eventKey;
    @XStreamAlias("Ticket")
    @XStreamConverter(XStreamCDataConverter.class)
    private String ticket;
    @XStreamAlias("MsgID")
    private Long msgId;
    @XStreamAlias("Status")
    @XStreamConverter(value = XStreamCDataConverter.class)
    private String status;
}
public class XStreamCDataConverter extends StringConverter {

    public XStreamCDataConverter() {
    }

    public String toString(Object obj) {
        return "<![CDATA[" + super.toString(obj) + "]]>";
    }
}

类型转换

    @Test
    void xml2ObjectByXStream() {

        XStream xStream = new XStream();
        XStream.setupDefaultSecurity(xStream);
        xStream.allowTypes(new Class[]{XmlMessage.class});
        xStream.processAnnotations(XmlMessage.class);

        XmlMessage message = (XmlMessage)xStream.fromXML(xmlString);
        log.info(message.toString());
    }

w3c.Dom

类型转换

    @Test
    void xml2ObjectByDom() throws Exception{
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        InputSource is = new InputSource(new StringReader(xmlString));
        Document document = builder.parse(is);

        // xml标签
        Element rootElement = document.getDocumentElement();
        NodeList childNodes = rootElement.getChildNodes();
        Map<String, Object> map = new HashMap<>(16);
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node node = childNodes.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element child = (Element) node;

                String nodeName = child.getNodeName();
                String textContent = child.getTextContent();
                map.put(nodeName, textContent);
            }
        }

        log.info(map.toString());
    }

dom4j

maven 依赖

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

类型转换

    @Test
    void xml2ObjectByDom4j() throws Exception{
        Map<String, String> map = new HashMap<>(16);
        SAXReader saxReader = new SAXReader();
        org.dom4j.Document doc = saxReader.read(new StringReader(xmlString));
        org.dom4j.Element root = doc.getRootElement();
        List<org.dom4j.Element> elements = root.elements();
        for (org.dom4j.Element element : elements) {
            map.put(element.getName(), element.getTextTrim());
        }

        log.info(map.toString());
    }

Json 对象如何适配字段格式?

微信公众号默认的模板消息结构如下

{
    "touser": "oHOLJw-r6lBxSXU4pRDKpoDyqWI0",
    "template_id": "7BsKKP1MsDxbNmfYfTaNVm9guRvgwH8l2PmFbDCMip0",
    "url": "http://idea360.cn",
    "data": {
        "name": {
            "value": "登高射太阳!",
            "color": "#173177"
        }
    }
}

消息占位符字典的形式很难扩展,所以我们想办法把它变为 List 集合处理

maven 依赖

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>

消息结构定义

@Data
public class WeixinTemplateMessage implements Serializable {
    private String toUser;
    private String templateId;
    private String url;
    private List<WeixinTemplateData> data;
}
@Data
public class WeixinTemplateData implements Serializable {
    private String key;
    private String value;
    private String color;
}

类型转换适配器

public class TemplateMessageGsonAdapter implements JsonSerializer<WeixinTemplateMessage> {
    @Override
    public JsonElement serialize(WeixinTemplateMessage message, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject messageJson = new JsonObject();
        messageJson.addProperty("touser", message.getToUser());
        messageJson.addProperty("template_id", message.getTemplateId());
        messageJson.addProperty("url", message.getUrl());

        JsonObject data = new JsonObject();
        messageJson.add("data", data);

        WeixinTemplateData item;
        JsonObject dataJson;
        for(Iterator itemData = message.getData().iterator(); itemData.hasNext(); data.add(item.getKey(), dataJson)) {
            item = (WeixinTemplateData)itemData.next();
            dataJson = new JsonObject();
            dataJson.addProperty("value", item.getValue());
            dataJson.addProperty("color", item.getColor());
        }

        return messageJson;
    }
}

gson 注册转换器

public class WeixinGsonBuilder {

    private static final GsonBuilder INSTANCE = new com.google.gson.GsonBuilder();

    public WeixinGsonBuilder() {
    }

    public static Gson create() {
        return INSTANCE.create();
    }

    static {
        INSTANCE.registerTypeAdapter(WeixinTemplateMessage.class, new TemplateMessageGsonAdapter());
    }
}

格式转换

    @Test
    void gsonAdapter() {

        WeixinTemplateMessage weixinTemplateMessage = new WeixinTemplateMessage();
        weixinTemplateMessage.setTemplateId("7BsKKP1MsDxbNmfYfTaNVm9guRvgwH8l2PmFbDCMip0");
        weixinTemplateMessage.setToUser("oHOLJw-r6lBxSXU4pRDKpoDyqWI0");
        weixinTemplateMessage.setUrl("http://idea360.cn");

        WeixinTemplateData weixinTemplateData = new WeixinTemplateData();
        weixinTemplateData.setKey("name");
        weixinTemplateData.setValue("当我遇上你");
        weixinTemplateData.setColor("#ff0000");

        weixinTemplateMessage.setData(Arrays.asList(weixinTemplateData));

        String message = WeixinGsonBuilder.create().toJson(weixinTemplateMessage);
        log.info(message);
    }

结果输出发现就是前边我们需要的 json 格式。

消息推送

根据上述 gson 对模板消息的处理。参照 微信公众号接口学习 一文,即可轻松实现模板消息的推送。

模板没有创建接口,在公众号后台配置即可。

关注与取关

关注与取关的实现都是基于微信的事件推送来实现的。上述关于 xml 消息的处理即是为这里做铺垫。微信默认的推送消息是 xml 格式的。这里需要注意的是: 认证后的公众号有 EncodingAESKey, 即可实现消息的加密选项。

微信事件推送接口和签名校验接口地址相同,区别在于时间推送接口基于 POST

对于事件推送的处理的伪代码如下:

    @Override
    public void handleEvent(String encType, String timestamp, String nonce, String signature, String xmlMessage, String msgSignature) throws WxException {

        logger.info("收到微信推送消息:encType={}, timestamp={}, nonce={}, signature={}, xmlMessage={}", encType, timestamp, nonce, signature, xmlMessage);

        WxMpXmlMessage message = null;
        XStream xStream = new XStream();
        XStream.setupDefaultSecurity(xStream);
        xStream.allowTypes(new Class[]{WxMpXmlMessage.class});
        xStream.processAnnotations(WxMpXmlMessage.class);
        if (encType == null) {
            // 明文传输的消息
            message = (WxMpXmlMessage)xStream.fromXML(xmlMessage);

        } else if ("aes".equalsIgnoreCase(encType)) {
            // aes加密的消息
            WxMpCryptUtil cryptUtil = new WxMpCryptUtil(wxMpService.getWxMpConfigStorage());
            String plainText = cryptUtil.decrypt(msgSignature, timestamp, nonce, xmlMessage);
            logger.debug("解密后的原始xml消息内容:{}", plainText);

            message = (WxMpXmlMessage)xStream.fromXML(plainText);

        }


        if (message.getMsgType().equals("event")) {
            wxMpEventHandlerFactory.handler(message.getEvent(), message);
        }

    }

这里由于事件比较多,我们可以基于 工厂+策略 的模式来做业务处理。

  1. 定义 handler 接口
public interface WxMpEventHandler {

    String event();

    void handler(WxMpXmlMessage message) throws WxException;
}
  1. handler 实现
@Component
public class SubscribeHandler implements WxMpEventHandler {

    private static final Logger logger = LoggerFactory.getLogger(SubscribeHandler.class);

    @Override
    public String event() {
        return "subscribe";
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void handler(WxMpXmlMessage message) throws WxException {
        // 可以获取个人信息,绑定系统账户
        logger.info("有新用户关注:{}", userInfo);

    }
}
  1. 工厂类实现
@Component
public class WxMpEventHandlerFactory implements ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(WxMpEventHandlerFactory.class);
    private static Map<String, WxMpEventHandler> handlerMap = new HashMap<>();

    public WxMpEventHandlerFactory() {
    }

    public void handler(String event, WxMpXmlMessage message) throws WxException {
        WxMpEventHandler eventHandler = handlerMap.get(event);
        if (null != eventHandler) {
            eventHandler.handler(message);
        } else {
            logger.error("暂无该类型消息处理器");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, WxMpEventHandler> beansOfType = applicationContext.getBeansOfType(WxMpEventHandler.class);
        for (WxMpEventHandler bean : beansOfType.values()) {
            handlerMap.put(bean.event(), bean);
        }
    }
}

  1. 事件处理
        if (message.getMsgType().equals("event")) {
            wxMpEventHandlerFactory.handler(message.getEvent(), message);
        }

最后

本篇到此结束。代码比较多,节选了关键代码。大家如有疑问可公众号【当我遇上你】后台留言。感谢阅读。