
1.前言
前面我给大家讲了如何通过微信公众号发送模板消息 ,但是那个案例只能做群发消息。
但是在实际场景中,有些企业需要给指定用户发送特定消息。比如瑞幸咖啡给你发送了优惠券、顺丰会员等级变更通知、生活缴费通知、快递物流信息等等

那么这时候就需要将微信公众号的用户信息和第三方系统的用户进行绑定。
接下来我给大家讲一下第3方系统扫码绑定微信公众号的核心步骤:
- 1.第三方系统根据微信官方提供的 api 生成一个二维码
- 2.用户扫码关注公众号
- 3.微信公众号根据你配置的自己的服务器地址进行回调,回调信息包含用户操作事件(关注、已关注扫码、取消关注)和 openid
- 4.将 openid 存到数据库里面,和本地用户信息进行绑定
- 5.接下来就可以根据某个用户的 openid 和模板 id 发送特定消息了。

2.服务器配置

- URL 就是你自己公司服务器的接口地址,这里只支持 80 和 443 接口。
- Token 随便写一个,一定要记住
- EncodingAESKey 随机生成一个
- 消息加解密方式建议安全模式,也就是进行加密
在点击提交之前需要在后台提供一个校验接口:校验成功需要原样返回 echostr 参数内容

后台用的SpringBoot 项目:

Controller:
css
@GetMapping(value = "/checkWechat")
public String checkWechatLink(HttpServletRequest request) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(nonce)) {
return "";
}
// 验证消息的确来自微信服务器
wechatService.checkSignature(signature, timestamp, nonce);
// 原样返回echostr参数
return echostr;
}
Service:
java
/**
* 公众号服务器配置校验
* @param signature
* @param timestamp
* @param nonce
*/
@Override
public void checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[] {token, timestamp, nonce};
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (String str : arr) {
content.append(str);
}
String tmpStr = DigestUtils.sha1Hex(content.toString());
if (tmpStr.equals(signature)) {
log.info("check success");
return;
}
log.error("check fail");
throw new RuntimeException("check fail");
}
运行项目之后,服务器配置页面点击提交按钮

注:因为URL只支持80和443端口,所以在本地测试的时候可以使用贝锐花生壳进行内网穿透。

3.后台生成二维码,前端进行显示
3.1 后端
SpringBoot 环境配置:
引入微信官方依赖
java
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>3.5.0</version>
</dependency>
配置 application.yml

css
wechat:
appId: xxxx
appSecret: xxxx
tokenUrl: https://api.weixin.qq.com/cgi-bin/token
tokenGrantType: client_credential
templateUrl: https://api.weixin.qq.com/cgi-bin/message/template/send
userListUrl: https://api.weixin.qq.com/cgi-bin/user/get
userInfoUrl: https://api.weixin.qq.com/cgi-bin/user/info
token: xxxx
aesKey: xxxx
qrcodeUrl: https://api.weixin.qq.com/cgi-bin/qrcode/create
showQrcode: https://mp.weixin.qq.com/cgi-bin/showqrcode
获取微信公众号二维码接口:
Controller:
java
@GetMapping("/getQrCode")
public Result getUserPage() {
WechatQrCode qrCode = wechatService.getQrCode();
return Result.success(qrCode);
}
Service:
java
@Override
public WechatQrCode getQrCode() {
SysUser userInfo = SecurityUtil.getUserInfo();
JSONObject jsonObject = new JSONObject();
jsonObject.put("expire_seconds",QR_CODE_TICKET_TIMEOUT);
jsonObject.put("action_name","QR_STR_SCENE");
JSONObject sceneObject = new JSONObject();
JSONObject sceneStrObject = new JSONObject();
sceneStrObject.put("scene_str",userInfo.getUsername());
sceneObject.put("scene",sceneStrObject);
jsonObject.put("action_info",sceneObject);
// 获取 access_token
String accessToken = getAccessToken();
String requestUrl = wechatConfig.getQrcodeUrl()+"?access_token=" + accessToken;
String result = HttpUtil.createPost(requestUrl).body(jsonObject.toJSONString()).execute().body();
log.info("getQrCodeResult:{}",result);
JSONObject jsonResult = JSONObject.parseObject(result);
// 返回二维码信息
WechatQrCode wechatQrCode = new WechatQrCode();
wechatQrCode.setUrl(jsonResult.getString("url"));
wechatQrCode.setTicket(jsonResult.getString("ticket"));
wechatQrCode.setExpireSeconds(jsonResult.getLong("expire_seconds"));
wechatQrCode.setQrCodeUrl(wechatConfig.getShowQrcode()+"?ticket=" + URI.create(wechatQrCode.getTicket()).toASCIIString());
// 将二维码存入Redis,map 格式: key 是 ticket,map:{username:"",openid:""}
return wechatQrCode;
}
获取 AccessToken 方法:
java
@Override
public String getAccessToken() {
// 先判断 Redis 中是否存在
String wechat_access_token = stringRedisTemplate.opsForValue().get("wechat_access_token");
if(StrUtil.isBlank(wechat_access_token)){
String accessTokenUrl = wechatConfig.getTokenUrl()+"?grant_type=client_credential&";
String requestUrl = accessTokenUrl + "appid=" + wechatConfig.getAppId() + "&secret=" + wechatConfig.getAppSecret();
String result = HttpUtil.get(requestUrl);
log.info("wechat_access_token:{}",result);
JSONObject jsonObject = JSONObject.parseObject(result);
wechat_access_token = jsonObject.getString("access_token");
// 将 access_token 存入 Redis
stringRedisTemplate.opsForValue().set("wechat_access_token", wechat_access_token, 7000, TimeUnit.SECONDS);
}
return wechat_access_token;
}

讲解:
- 1.请求获取微信二维码地址:注意是 POST 请求
ini
https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
- 2.POST数据例子
json
{"expire_seconds": 604800,
"action_name": "QR_STR_SCENE",
"action_info":
{"scene":
{"scene_str": "test"}}}
这里我把用户的编号放进了 scene_str 的参数值里面,当然你也可以放用户的id。

- 3.返回结果

后端主要就是将 mp.weixin.qq.com/cgi-bin/sho... 和 ticket 拼接成一个二维码图片地址返回给前端:

3.2 前端展示并轮询状态

点击绑定按钮获取公众号二维码地址,通过 ElementUI 的 el-popover 和 el-image 进行展示:
css
<span style="font-size: 15px; float: right; margin: 15px 10px"
>绑定公众号:
<el-popover
:visible="bindWechatVisible"
transition="el-zoom-in-top"
placement="left-end"
title="绑定知否技术公众号"
:width="200"
trigger="click"
>
<template #reference>
<el-tag
style="cursor: pointer"
v-if="!userForm.openId"
size="small"
@click="getWechatQrcode"
>绑定</el-tag
>
</template>
<el-image :src="wechatUrl" style="width: 180px; height: 180px" :fit="fit" />
<div style="text-align: right; margin: 0">
<el-button size="small" type="primary" plain @click="getWechatQrcode"
>刷新</el-button
>
<el-button size="small" type="primary" @click="bindWechatVisible = false"
>关闭</el-button
>
</div>
</el-popover>
<el-tag v-if="userForm.openId" type="success" size="small">已绑定</el-tag>
</span>
获取二维码接口:
css
// 获取微信二维码
const bindWechatVisible = ref(false);
const wechatUrl = ref(null);
const getWechatQrcode = async () => {
const res = await wechatApi.createQrcode();
wechatUrl.value = res.data.data.qrCodeUrl;
bindWechatVisible.value = true;
// 检验用户绑定公众号状态
checkBindOpenid();
};

生成二维码之后立即轮询校验用户是否绑定微信公众号,其实就是校验系统数据库是否设置了 openid
css
let timer = null;
const checkBindOpenid = () => {
timer = setInterval(async () => {
const res = await wechatApi.checkBindOpenid();
// 如果已经绑定
if (res.data.data && res.data.data !== "") {
// 清除轮询
clearInterval(timer);
// ......业务逻辑
bindWechatVisible.value = false;
}
}, 1500);
};
后台校验用户绑定公众号接口:就是查询数据库这个用户是否设置 openid
java
/**
* 校验用户是否绑定公众号
* @return
*/
@GetMapping("/checkBindOpenid")
public Result checkBind() {
String openid = wechatService.checkBindOpenid();
return Result.success(openid);
}
4.处理微信公众号的回调信息(核心逻辑)
和公众号服务器配置校验的接口路径一样,只不过多了一个核心的 requestBody 参数,这里面包含详细的回调信息:
java
@PostMapping("/checkWechat")
public String checkWechat(@RequestBody String requestBody, @RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce) {
log.info("requestBody:{}", requestBody);
log.info("signature:{}", signature);
log.info("timestamp:{}", timestamp);
log.info("nonce:{}", nonce);
// 处理微信服务器推送的消息
wechatService.handleWechatMsg(requestBody);
return "success";
}
处理回调消息核心逻辑:
java
/**
* 处理微信推送的消息
* @param body
* @return
*/
@Override
public String handleWechatMsg(String body) {
WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(body);
String eventKey = wxMpXmlMessage.getEventKey();
String openId = wxMpXmlMessage.getFromUser();
log.info("wxMpXmlMessage:{}",wxMpXmlMessage);
log.info("openId:{}",openId);
log.info("eventKey:{}",eventKey);
log.info("event:{}",wxMpXmlMessage.getEvent());
// 关注
if(wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_SUBSCRIBE)){
log.info("event:{}",wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_SUBSCRIBE));
// eventKey:qrscene_admin
String username = eventKey.split("_")[1];
// 绑定 openid
sysUserService.lambdaUpdate().eq(SysUser::getUsername,username)
.set(SysUser::getOpenId,openId).update();
}
// 已关注,扫描
if( wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_SCAN)){
String username = eventKey;
SysUser currentUser = sysUserService.lambdaQuery().eq(SysUser::getUsername, username)
.one();
if(StrUtil.isBlank(currentUser.getOpenId())){
sysUserService.lambdaUpdate().eq(SysUser::getUsername,username)
.set(SysUser::getOpenId,openId).update();
}
return "success";
}
// 取消关注
if(wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_UNSUBSCRIBE) ){
log.info("event:{}",wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_UNSUBSCRIBE));
// 取消绑定 openid
sysUserService.lambdaUpdate().eq(SysUser::getOpenId,openId)
.set(SysUser::getOpenId, null).update();
}
return XmlUtil.toStr( XmlUtil.beanToXml(wxMpXmlMessage));
}
body 参数是 xml 格式的,所以需要用官方提供的接口进行解析:
java
WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(body);
返回的数据格式有3种:
- 关注 subscribe
json
{EventKey":"qrscene_admin","Event":"subscribe","FromUserName":"xxxxxxx"}
- 已关注,扫码 SCAN
json
{"EventKey":"admin","Event":"SCAN","ToUserName":"xxxxx","FromUserName":"xxxxx"}
- 取消关注 unsubscribe
json
{"event":"unsubscribe","eventKey":"","fromUser":"xxxx"}
其中最重要的 openid
css
String openId = wxMpXmlMessage.getFromUser();
-
当 event 是 subscribe 时,eventKey 格式是 qrscene_admin,下划线后面就是前面我们设置的用户编号。
-
当 event 是 SCAN 时,eventKey 就是 admin ,用户编号。
-
当 event 是 unsubscribe 时,只返回取消用户的 openid,我们可以将自己系统数据库的用户 openid 设置为 null。
5.给特定用户发送模板消息
在前面文章中已经讲过发送模板消息,这里不再贴完整代码。
java
JSONObject sendGoodsObject = new JSONObject();
// 用户名
sendGoodsObject.put("thing5", JSONUtil.createObj().putOpt("value", filterUser.getName()));
// 金额
sendGoodsObject.put("amount3", JSONUtil.createObj().putOpt("value", amount));
// 当前余额
sendGoodsObject.put("amount4", JSONUtil.createObj().putOpt("value", balance));
// 日期
sendGoodsObject.put("time1", JSONUtil.createObj().putOpt("value", date));
JSONObject jsonWechatResult = wechatService.sendWechatMessage(returnFundObject,user.getOpenId(),"xxxxxxxxxx","https://www.baidu.com");
// 如果微信公众号发送成功
if("ok".equals(jsonWechatResult.get("errmsg"))){
// 成功之后保存发送的信息
TextMessage textMessage = new TextMessage();
textMessage.setTel(tel);
textMessage.setType(type);
textMessage.setAmount(amount);
textMessage.setBalance(balance);
textMessage.setDealerName(dealerName);
textMessage.setManager(manager);
textMessage.setSendTime(date);
// 保存发送的信息
textMessageService.save(textMessage);
}