跳到主要内容

微信小程序消息推送

阅读需 7 分钟

需求背景

管理员在后台给小程序用户推送一些消息,例如用户收费通知,工单维护通知等。

功能流程图

微信用户消息推送流程图

功能实现

小程序项目添加如下代码,开启用户订阅

用户订阅消息文档: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);
}
})
  1. 微信开发工具显示用户订阅消息弹窗和真机调试中,显示得内容不一样,注意甄别。
  2. 用户在订阅消息之后,点击下面得对号就是永久订阅,可以在小程序设置中关闭。(实际还是走了订阅,但是用户无感)
  3. 用户订阅消息一定要通过用户点击事件或者支付回调触发,在 await 中不生效。
  4. wx.getSetting() 接口可获取用户对相关模板消息的订阅状态。
  5. 在触发用户订阅消息事件之后,微信服务器会发送验证请求到开发者服务器进行信息验证。

后台功能

功能描述

后台功能主要有以下三个:

  1. 接受微信服务器得用户订阅消息验证;
  2. 验证通过后,接受微信服务器得用户订阅消息事件;
  3. 发送消息给指定 openId 用户

代码实现

  1. 微信服务器权限验证

    微信权限验证策略:

    将自定义 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 "";
    }
    }
  2. 微信用户订阅事件接受接口

    用户事件接口必须和权限验证接口同名,微信服务器会发送 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());
    }

    }
  3. 实体

    @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;
    }

至此,微信小程序服务器的交互功能就完成了。

微信小程序消息开通和发送消息

消息发送后台功能实现

  1. 消息发送器

    后台发送消息时,需要获取 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
    );
    }
    }
  2. 发送微信消息

    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);
    }

    }
  3. 在业务逻辑中调用

    一般需要获取通知用户的 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);
}

实际使用

实际使用需要在业务代码逻辑中调用。

效果预览

微信小程序接收如下:

微信小程序接收

Loading Comments...