目录
一、微信模版消息
先上一张微信公众号模版消息的截图,直观感受一下什么是模版消息:
类似上面这种通知消息,能够给用户发送某种特定通知,以达到提醒用户的作用。截用微信官方给的模版消息介绍:
二、模版消息怎么用
开通模板消息功能
首先,需要在微信公众号平台注册并认证,然后开通模板消息功能。具体步骤包括进入微信公众号平台,查看是否已获取模板消息权限,并在模板消息模块中添加模板。
若在左侧菜单中找不到模版消息,可以在“新功能”菜单中添加
配置模板消息
选择合适的模板后,需要配置模板中的具体信息,如发送内容、跳转链接等。选择好模提交模板消息后,需要经过微信官方的审核。审核通过后,才能进行消息的群发。 以下是我申请的一个消息模版
熟悉发送模版消息接口
在模版消息审核通过后,我们就可以着手对接代码功能,准备下发消息;
我们先查看一下微信官方给出的发送模版消息接口:
http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
post发送入参数据:
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"url":"http://weixin.qq.com/download",
"miniprogram":{
"appid":"xiaochengxuappid12345",
"pagepath":"index?foo=bar"
},
"client_msg_id":"MSG_000001",
"data":{
"keyword1":{
"value":"巧克力"
},
"keyword2": {
"value":"39.8元"
},
"keyword3": {
"value":"2014年9月22日"
}
}
}
参数说明
touser : 微信公众号的openId(一定是微信公众号的openID,不是小程序的)。
template_id:这个就是我们上面申请到的模版ID,完全复制过来,不要弄错。
appid:所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏)。
pagepath: 跳转到小程序页面的路径。这里有个点要注意,要跳转的小程序必须是发布上线的,调试模式的小程序不支持跳转,会报40165错误。
data: 安装申请的模版参数进行填充,可以参考类目模板消息参数值内容限制说明。
如何获取微信公众号的openId
一般来说,模版消息都只会是给已关注了公众号的用户发送,在用户关注公众号时,我们可以通过微信回调接口,保存用户的openId。具体操作步骤:
1、进入公众号,找到“设置与开发-基本配置”,如下图:
配置完服务器地址后,还需要配置地址对应的IP白名单,有时添加白名单后,需要等待几分到十几分钟后才会生效;
注意:在启用了服务器配置后,在公众号中“自动回复”和“自定义菜单功能”全部失效,我们可以通过后台接口进行配置。
参考自定义菜单https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
添加我们服务器的接口地址,用于给微信处理事件回调,接口的方法名任意,必须实现GET和POST两种接口:
2、代码接收微信事件回调:
/**
* 验证只接受微信后台的服务请求
* 开发者通过检验signature对请求进行校验。
* 若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功。
* 验证流程如下:
* 1、将token timestamp nonce三个参数进行字典序排序;
* 2、将三个参数字符串拼接成一个字符串进行sha1加密;
* 3、将加密后的字符串与signature对比,相同则该请求来源于微信。
*/
@GetMapping( "/official/authenticate/{appKey}")
public String authenticate(@PathVariable("appKey") String appKey,@RequestParam String signature, @RequestParam String timestamp,
@RequestParam String nonce, @RequestParam String echostr) {
boolean result = checkSignature(signature, timestamp, nonce);
if (result) {
return echostr;
}
return "FAILURE";
}
/**
* 微信消息响应,通过公众号-基本配置-服务器配置,填写回调地址
*
* @param request 请求信息
* @return java.lang.String
*/
@PostMapping( "/official/authenticate/{appKey}")
public String authenticate(@PathVariable("appKey") String appKey, HttpServletRequest request, HttpServletResponse response){
return authenticateOfficial(request,appKey);
}
public boolean checkSignature(String signature, String timestamp, String nonce) {
String[] array = new String[]{ "微信公众号配置的Token", timestamp, nonce};
//先对这三个字符串字典排序,然后拼接
Arrays.sort(array);
StringBuilder sb = new StringBuilder();
for (String s : array) {
sb.append(s);
}
//使用SHA-1算法对拼接字符串加密
MessageDigest messageDigest;
String hexStr = null;
try {
messageDigest = MessageDigest.getInstance("SHA-1");
byte[] digest = messageDigest.digest(sb.toString().getBytes());
//将加密后的字符串转换成16进制字符串
hexStr = CommonUtils.bytesToHexStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
//返回校验结果
return signature.equalsIgnoreCase(hexStr);
}
@NotNull
private String authenticateOfficial(HttpServletRequest request,String appKey) {
val msg = MessageUtil.parseRequest(request);
// 扫码或关注公众号
log.info("wechat callback message=[{}]", JsonUtil.toJson(msg));
//消息类型
MsgTypeEnum msgTypeEnum = MsgTypeEnum.getByType((String) msg.get("Event"));
if (null == msgTypeEnum) {
return StrUtil.EMPTY;
}
//根据不同消息类型做不同处理
try {
switch (msgTypeEnum) {
case CLICK:
String eventKey = (String) msg.get("EventKey");
log.info("菜单点击事件:{}",eventKey);
String eventMsg = wechatService.handleClickEvent(msg, appKey);
log.info("点击事件{}后回复:{}",eventKey,eventMsg);
return eventMsg;
case EVENT:
break;
case SUBSCRIBE:
//处理关注事件
subscribeService.doSubscribeEvent(msg, msgTypeEnum,appKey);
//关注后发送消息
String mapToXml = wechatService.sendSubscribeMessage(msg,appKey);
log.info("关注后消息回复:{}",mapToXml);
return mapToXml;
case UNSUBSCRIBE:
subscribeService.doSubscribeEvent(msg, msgTypeEnum,appKey);
break;
default:
break;
}
}catch (Exception e){
log.error("处理微信公众号请求异常:", e);
}
return StrUtil.EMPTY;
}
在post的方法中处理微信的回调事件,当用户在微信界面点击关注了公众号后,会回调到我们上面的POST接口中,事件为subscribe:
解析微信事件代码,此处有点需要注意,如果在springboot工程中开启了防XSS攻击,一定要把回调的post接口设为白名单放过,不然解析不到正确的数据:
@Slf4j
public class MessageUtil {
/**
* 解析微信公众号回调xml
* @param request 请求
* @return
*/
public static Map<String,Object> parseRequest(HttpServletRequest request){
InputStream inputStream = null;
Map<String,Object> map = new HashMap<>();
SAXReader reader = new SAXReader();
Document document;
try {
inputStream = request.getInputStream();
//读取输入流获取文档对象
document = reader.read(inputStream);
//根据文档对象获取根节点
Element rootElement = document.getRootElement();
//获取根所有子节点
List<Element> elements = rootElement.elements();
for (Element e:elements) {
map.put(e.getName(),e.getStringValue());
}
} catch (IOException | DocumentException e) {
log.error("处理微信公众号请求异常:", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ioe) {
log.error("关闭inputStream异常:", ioe);
}
}
}
return map;
}
}
处理订阅(subscribe),此时就可以解析到微信公众号的openId了。
发送模版消息
在获得了openId后,就可以集成发送模版消息的接口,我们也分以下几个步骤处理:
1、通过微信公众号appId和secret 获取微信的ACCESS_TOKEN,access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。另外使用该接口也有次数限制默认好像是2000次/天。所以建议最好用缓存处理。
public String getOfficialAccessToken(String appId, String appSecret) {
String redisKey = RedisKeyBuilder.getOfficialTokenKey(appId);
log.info("微信公众号access_token redisKey:{}", redisKey);
Object cachToken = redisUtil.get(redisKey);
long expire = redisUtil.getExpire(redisKey);
log.info("微信公众号access_token过期时间:{}", expire);
log.info("微信公众号access_token:{}", cachToken);
log.info("微信公众号appid:{}", appId);
log.info("微信公众号secret:{}", appSecret);
//判断不存在或者过期,从腾讯服务重新获取
if (cachToken == null || StringUtils.isEmpty(cachToken.toString())) {
log.info("获取公众号token");
String getAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
try {
getAccessTokenUrl = getAccessTokenUrl.replace("APPID", appId);
getAccessTokenUrl = getAccessTokenUrl.replace("APPSECRET", appSecret);
String data = HttpUtil.get(getAccessTokenUrl);
JSONObject jsonObject = JSONUtil.parseObj(data);
log.info("公众号获取token:{}", jsonObject);
String accessToken = jsonObject.getStr("access_token");
//过期时间7200s
Integer expiresTime = jsonObject.getInt("expires_in");
if (StringUtils.isNotEmpty(accessToken)) {
//将token保存到缓存中
redisUtil.set(redisKey, accessToken, expiresTime - 60, TimeUnit.SECONDS);
}
return accessToken;
} catch (Exception e) {
log.error("错误信息:{}", e.getMessage());
}
}
assert cachToken != null;
return cachToken.toString();
}
2、封装微信模版消息,调用发送消息接口
/**
*
* @param message 消息体替换对象
* @param templateId 消息模板ID
* @param page 小程序调整页地址
* @param openId 消息接收用户
*/
public void messageSend(WxMessageBean message, MiniProgram miniMessageBean , String templateId, String page, String openId) {
miniMessageBean.setAppid(weChatProperties.getMiniAppId());
boolean notEmpty = StringUtils.isNotEmpty(miniMessageBean.getPagepath());
miniMessageBean.setUsepath(true);
if(!notEmpty){
miniMessageBean.setPagepath("pages/index/index");
}
String token = (String) tokenCache.getIfPresent(CACHE_TOKEN_KEY);
if (token == null) {
token = this.getAccessToken();
}
String url = weChatProperties.getUniformSend() + token;
// 这里的参数要和下面的Map Key值对应
JSONObject jsonObject = new JSONObject();
jsonObject.put("touser", openId);
jsonObject.put("template_id",templateId);
jsonObject.put("miniprogram",miniMessageBean);
jsonObject.put("url", StringUtils.isNotEmpty(page)?page:weChatProperties.getPagePath());
jsonObject.put("data", message);
log.info("消息{}", jsonObject);
ResponseEntity<String> forObject = restTemplate.postForEntity(url, jsonObject, String.class);
JSONObject object = JSON.parseObject(forObject.getBody());
Integer errcode = (Integer) object.get("errcode");
//微信token失效重试
if(errcode == 40001){
tokenCache.invalidate(CACHE_TOKEN_KEY);
messageSend(message, miniMessageBean, templateId, page, openId);
return;
}
if (errcode == 0) {
log.info("消息推送成功");
} else if (errcode == 40003) {
log.error("推送消息的openid错误,openid:{},消息内容:{}", openId, message);
} else if (errcode == 43004) {
log.error("该用户未关注公众号,openid:{},消息内容:{}", openId, message);
} else {
log.error("消息推送失败:" + errcode);
}
}
完成上面所有操作后,程序中没有报错,就可以到手机微信中,查看到刚刚发送的模版消息了。
结语
发送微信模版消息的过程其实并不复杂,主要是我们事先需要熟悉微信的相关文档。不过在其开发过程中会遇到各种的意想不到的状况(坑)。我想在看我上面我梳理的流程后,大家会更清晰些!
另外,在实际场景中,我们是要根据业务需求,把模版消息推送到我们业务中具体的某个/某类人。那我们的业务系统中的用户,怎么和微信公众号用户关联起来呢?欢迎关注下篇...
最后,最后说明下,上面的代码只是为了说明流程性的东西,可能不能直接正常运行,大家如果在实际开发中遇到问题,欢迎留言,我会第一时间回复。感谢!!!