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

前言

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

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

xstream

maven依赖

1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.12</version>
</dependency>

消息原型(xmlString)

1
2
3
4
5
6
7
8
<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对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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;
}
1
2
3
4
5
6
7
8
9
public class XStreamCDataConverter extends StringConverter {

public XStreamCDataConverter() {
}

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

类型转换

1
2
3
4
5
6
7
8
9
10
11
@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

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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依赖

1
2
3
4
5
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
@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对象如何适配字段格式?

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

1
2
3
4
5
6
7
8
9
10
11
{
"touser": "oHOLJw-r6lBxSXU4pRDKpoDyqWI0",
"template_id": "7BsKKP1MsDxbNmfYfTaNVm9guRvgwH8l2PmFbDCMip0",
"url": "http://idea360.cn",
"data": {
"name": {
"value": "登高射太阳!",
"color": "#173177"
}
}
}

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

maven依赖

1
2
3
4
5
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>

消息结构定义

1
2
3
4
5
6
7
@Data
public class WeixinTemplateMessage implements Serializable {
private String toUser;
private String templateId;
private String url;
private List<WeixinTemplateData> data;
}
1
2
3
4
5
6
@Data
public class WeixinTemplateData implements Serializable {
private String key;
private String value;
private String color;
}

类型转换适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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注册转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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());
}
}

格式转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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接口
1
2
3
4
5
6
public interface WxMpEventHandler {

String event();

void handler(WxMpXmlMessage message) throws WxException;
}
  1. handler实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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. 工厂类实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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. 事件处理
1
2
3
if (message.getMsgType().equals("event")) {
wxMpEventHandlerFactory.handler(message.getEvent(), message);
}

最后

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