需求背景
管理员在后台给小程序用户推送一些消息,例如用户收费通知,工单维护通知等。
功能流程图
功能实现
小程序项目添加如下代码,开启用户订阅
用户订阅消息文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html
在小程序中加入如下代码,开启用户订阅:
uni.requestSubscribeMessage({
// 模板消息 id
tmplIds: ['tmpId'],
success(res) {
console.log(res);
},
fail(err) {
console.log(err);
}
})
- 微信开发工具显示用户订阅消息弹窗和真机调试中,显示得内容不一样,注意甄别。
- 用户在订阅消息之后,点击下面得对号就是永久订阅,可以在小程序设置中关闭。(实际还是走了订阅,但是用户无感)
- 用户订阅消息一定要通过用户点击事件或者支付回调触发,在 await 中不生效。
- wx.getSetting() 接口可获取用户对相关模板消息的订阅状态。
- 在触发用户订阅消息事件之后,微信服务器会发送验证请求到开发者服务器进行信息验证。
后台功能
功能描述
后台功能主要有以下三个:
- 接受微信服务器得用户订阅消息验证;
- 验证通过后,接受微信服务器得用户订阅消息事件;
- 发送消息给指定 openId 用户
代码实现
-
微信服务器权限验证
微信权限验证策略:
将自定义 token 和 timestamp、nonce 按照字典序排列,之后 SHA1 加密和 signature 进行对比,相同返回 echostr。
// controller
/**
* 微信服务器鉴权接口
*
* @param signature 签名
* @param timestamp 时间戳
* @param nonce 随机数
* @param echostr 随机字符串
* @return 鉴权通过返回 echostr
*/
@GetMapping(value = "/user")
public String userAuth(String signature, String timestamp, String nonce, String echostr) {
return messageService.auth(signature, timestamp, nonce, echostr);
}
// service
public String auth(String signature, String timestamp, String nonce, String echostr) {
log.info("接收到的参数:{} {} {} {}", signature, timestamp, nonce, echostr);
String[] arr = new String[]{wxConfigProperties.getMsgToken(), timestamp, nonce};
Arrays.sort(arr);
StringBuilder sb = new StringBuilder();
for (String str : arr) {
sb.append(str);
}
String computedSignature = SHAUtil.sha1(sb.toString());
log.info("微信服务器接收到的 签名:{}", signature);
log.info("SHA1 计算出的签名:{}", computedSignature);
if (computedSignature.equals(signature)) {
return echostr;
} else {
return "";
}
} -
微信用户订阅事件接受接口
用户事件接口必须和权限验证接口同名,微信服务器会发送 GET 用于验证,POST 用于发送用户订阅事件消息。
// controller
/**
* 微信服务器处理用户事件接口
*/
@PostMapping(value = "/user")
public void user(HttpServletRequest request) {
messageService.auth(request);
}
// service
/**
* // 接口返回 JSON 和 XML 返回体可以在微信小程序自定义返回格式。
* {
* "ToUserName":"gh_ea84a199bf81",
* "FromUserName":"oG0NJ5Oi_3Dd1HlZJ14xfnA0sJ6s",
* "CreateTime":1686131943,
* "MsgType":"event",
* "Event":"subscribe_msg_popup_event",
* "List":{
* "PopupScene":"0",
* "SubscribeStatusString":"accept",
* "TemplateId":"4ondVRxk4L20ihrJ3iI15BDK72XatGPxE0MeCVwHasQ"
* // 选择明文加密兼容模式时,还会返回加密内容,方便调试。
* }
* }
*/
public void user(HttpServletRequest request) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder requestContent = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
requestContent.append(line);
}
reader.close();
if (!StringUtils.hasText(requestContent.toString())) {
throw new ServiceException("微信服务器响应失败!");
}
log.info("微信服务器响应:{}", requestContent);
JsonNode jsonNode = JsonUtils.fromJson(requestContent.toString(), new TypeReference<>() {
});
// 解析为 JSONNode
String toUserName = jsonNode.get("ToUserName").asText();
String fromUserName = jsonNode.get("FromUserName").asText();
long createTime = jsonNode.get("CreateTime").asLong();
String msgType = jsonNode.get("MsgType").asText();
String event = jsonNode.get("Event").asText();
JsonNode listNode = jsonNode.get("List");
String subscribeStatus = listNode.get("SubscribeStatusString").asText();
String templateId = listNode.get("TemplateId").asText();
// 需要保存的用户订阅消息实体信息
WeChatMsg dbWeChatMsg = this.weChatMsgMapper.selectOneByQuery(new QueryWrapper().eq(WeChatMsg::getFromUserName, fromUserName));
// 如果已经存在,则更新覆盖字段即可
if (Objects.nonNull(dbWeChatMsg)) {
dbWeChatMsg.setSubscribeStatus(subscribeStatus);
dbWeChatMsg.setTmplId(templateId);
dbWeChatMsg.setToUserName(toUserName);
this.weChatMsgMapper.update(dbWeChatMsg);
} else {
// 没有则插入数据
WeChatMsg weChatMsg = new WeChatMsg();
weChatMsg.setId(SnowFlakeIdGenerator.generateId());
weChatMsg.setToUserName(toUserName);
weChatMsg.setFromUserName(fromUserName);
weChatMsg.setCreateTime(String.valueOf(createTime));
weChatMsg.setMsgType(msgType);
weChatMsg.setEvent(event);
weChatMsg.setTmplId(templateId);
weChatMsg.setSubscribeStatus(subscribeStatus);
this.weChatMsgMapper.insert(weChatMsg);
}
} catch (IOException e) {
throw new ServiceException("接受微信服务器订阅消息异常:err: " + e.getMessage());
}
} -
实体
@Table(value = "tbl_wechat_sub")
public class WeChatMsg {
private Long id;
private String toUserName;
private String fromUserName;
private String createTime;
private String msgType;
private String event;
private String subscribeStatus;
private String tmplId;
}
至此,微信小程序服务器的交互功能就完成了。
微信小程序消息开通和发送消息
消息发送后台功能实现
-
消息发送器
后台发送消息时,需要获取 accessToken,一般2个小时刷新一次,需要动态获取。
@Component
public class WeChatSubscribeMessageSender {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(WeChatSubscribeMessageSender.class);
public void sendSubscribeMessage(String accessToken, String openid, String templateId, String page, String data) {
try {
String url = GarageConstants.WeChatConstants.MsgBaseUrl + "?access_token=" + accessToken;
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setDoOutput(true);
// 构建请求体
String body = buildRequestBody(openid, templateId, page, data);
byte[] requestBodyBytes = body.getBytes(StandardCharsets.UTF_8);
// 发送请求
connection.getOutputStream().write(requestBodyBytes);
// 读取响应
int responseCode = connection.getResponseCode();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 请求成功处理逻辑
logger.info("发送订阅消息成功. resp: {}", response);
} else {
// 请求失败处理逻辑
logger.info("发送订阅消息失败, 响应:{}", response);
}
connection.disconnect();
} catch (IOException e) {
throw new ServiceException("微信消息推送失败!err: " + e.getMessage());
}
}
/**
* 构建请求体的JSON字符串
*/
private String buildRequestBody(String openid, String templateId, String page, String data) {
return String.format(
"{\"touser\":\"%s\",\"template_id\":\"%s\",\"page\":\"%s\",\"data\":%s}",
openid, templateId, page, data
);
}
} -
发送微信消息
public class WeChatMsgHelper {
private static final Logger logger = LoggerFactory.getLogger(WeChatMsgHelper.class);
// 推送消息的时候需要 accessToken,这里是获取
// token 的方法,在实际环境中,一般是定时刷新,
// 两个小时内有效,获取accesstoken时需要appid和secret信息,一般是后台参数或常量进行配置
// 获取 accessToken 的请求接口
private final static String GetAccessToken = GarageConstants.WeChatConstants.TokenBaseUrl + "&appid={appid}&secret={secret}";
public static String refreshAccessToken(String appid, String secret, RestTemplate restTemplate){
if(!StringUtils.hasText(appid) || !StringUtils.hasText(secret)){
logger.error("刷新微信 AccessToken 时,缺少 appid 或 secret 参数!");
throw new ServiceException("刷新微信 AccessToken 时,缺少 appid 或 secret 参数!");
}
Map<String, Object> param = new HashMap<>();
param.put("appid",appid);
param.put("secret",secret);
ResponseEntity<JSONObject> resp = restTemplate.getForEntity(
GetAccessToken,
JSONObject.class,
param
);
JSONObject jsonObj = resp.getBody();
String accessToken = null;
if(jsonObj != null){
accessToken = jsonObj.getStr("access_token");
}
return accessToken;
}
public static Map<String, Object> createDataItem(String name, String value) {
Map<String, Object> item = new HashMap<>();
item.put("value", value);
return item;
}
/**
* 发送微信小程序消息
* @param configProperties 微信配置属性
* @param restTemplate restTemplate 模板
* @param openId 用户 openId
* @param templateId 模板 Id
* @param page 点击跳转的页面路径
* @param jsonData 发送的 json 数据
* @param messageSender 消息发送器
*/
public static void send(
WXConfigProperties configProperties,
RestTemplate restTemplate,
String openId,
String templateId,
String page,
String jsonData,
WeChatSubscribeMessageSender messageSender
) {
// 刷新 token
String accessToken = WeChatMsgHelper.refreshAccessToken(
configProperties.getAppId(),
configProperties.getAppSecret(),
restTemplate
);
messageSender.sendSubscribeMessage(accessToken, openId, templateId, page, jsonData);
}
} -
在业务逻辑中调用
一般需要获取通知用户的 openId,去数据库中查询到 templateId,之后组装消息然后发送给用户。
// 配置的 JSON 数据信息需要看后面的 微信模板参数。
JSONObject messageData = new JSONObject();
messageData.set("thing8", createDataItem("服务内容", projectInfoById.getName()));
messageData.set("thing5", createDataItem("备注", userNotify.getText()));
String jsonData = messageData.toJSONString(0);
// 组装发送消息需要的参数
WeChatMsgHelper.send(
configProperties,
restTemplate,
user.getOpenId(),
weChatMsg.getTmplId(),
"pages/index",
jsonData,
messageSender
);
小程序后台配置
消息推送配置
在小程序后台 -> 管理 -> 开发管理 -> 消息推送中配置后台服务器地址地址信息。
注意:在配置时需要发送验证到后台服务器验证,所以需要部署到公网可访问得环境中测试。
完成相关配置如下:
模板消息配置
小程序后台 -> 基础功能 -> 订阅消息 -> 在我的模板中选择模板配置。
测试
手动编写 controller 接口
/**
* 测试使用
* 模拟消息推送,写死所有的参数信息,实际中应该从数据库中查询到已经订阅消息的用户发送消息通知。
*/
@GetMapping("/push")
public void push() {
String openid = "ooaER7ctq6G_5CNrs0LE2vR2wCzk";
String templateId = "3gMbhH_bFuZlXiaJffCqXlNoqKvJwz_zUSmr-z1jEyo";
String page = "/pages/index";
JSONObject messageData = new JSONObject();
messageData.set("thing1", createDataItem("公司名称", "测试公司"));
messageData.set("thing4", createDataItem("地址", "天庭"));
messageData.set("name2", createDataItem("用户姓名", "测试-牧生"));
messageData.set("phone_number3", createDataItem("联系方式", "18198086793"));
messageData.set("thing19", createDataItem("报修原因", "测试消息推送"));
String jsonData = messageData.toJSONString(0);
WeChatMsgHelper.send(configProperties, restTemplate, openid, templateId, page, jsonData, messageSender);
}
实际使用
实际使用需要在业务代码逻辑中调用。
效果预览
微信小程序接收如下: