---
id: "20190507"
date: 2019/05/07 10:58:00
title: "小程序推送模板消息"
tags: ["小程序", "mpvue", "消息推送"]
categories:
  - "小程序"
---

  前段时间剁手了 PS4,在浏览商店时，发现官方商店真的不好用，主要是网络原因，次要是页面设计。所以就想自己做一个游戏查询的小程序，可以关注某个游戏，然后在这个游戏打折的时候发送通知给用户。最后发现有个很大的问题是：小程序没法直接给用户推送消息（当时还不知道模板消息），服务号才能。然后就用邮箱实现了通知功能，但是邮箱的局限实在是太大了（各大免费邮箱每天的发件数都很小，自己搭建的邮件服务器虽然没有发件限制，但是大概率会被放到垃圾箱）。

  然后某天在微信小程序的管理后台发现了模板消息这个东西，查了会资料发现可以通过这个来实现消息推送。要给用户发送模板消息需要`formId/prepay_id`这样一个东西，这个东西是怎么来的呢？

  <!-- more -->

  - formId:这个可以通过表单的提交来获取，需要在<form/>组件中设置属性`report-submit="true"`,这样每次对这个表单的提交一次就会产生一个 formId.
  - prepay_id:这个是支付动作产生的，具体的我也不太清楚，毕竟个人小程序并不能支付.

  下面具体来讲讲前后台的实现，前台使用 mpvue，后台使用 java

  # 前台获取 formId

  既然可以通过 form 的 submit 操作来获取到 formId,那我们稍微拓展一下，将我们的小程序页面中所有用户能点击的部分都用 form,button 来包裹一下，这样用户感知不到有表单提交操作，我们也能获取大量的 formId。

  注意不能用叠加的方式来一次点击获取多个 formId,这样方法已经不行了，获取到的都会是一样的。

  ## 获取一个模板

  要发送模板消息，首先要在小程序的管理后台上添加模板，步骤如下：

  1.在模板库中选择一个模板

  ![选择模板](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190507202930.png)

  2.选择显示参数

  选择要显示在消息中的参数，这里选择如下两个参数：

  ![选择参数](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190508101423.png)

  这样就有了一个模板可以用来发消息了，在我的模板中可以看到模板 id，和字段 id

  ![模板id](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190508101706.png)

  ## 获取 formId

  formId 是通过表单提交来获取到了，为了获取足够多的 formId,可以将能够点击的组件（比如按钮，列表单元..）包裹在 form 中，这样用户在日常使用中就能够收集到足够多的 formId。下面以包裹一个有赞的按钮为例。因为小程序的限制，设置`form-type`属性的按钮必须为 form 组件的直接子节点，所以并不能够在 form 中使用自定义组件，并将`form-type='submit'`设置到自定义组件中。

  template 部分如下：

  ```html
  <!-- 要获取formId，需要给form设置report-submit="true"的属性，然后在form-type="submit"的按钮上产生点击动作，才会触发表单提交的事件--formSubmit,进而获取到formId -->
  <form @submit="formSubmit" report-submit="true" class="bottom">
    <button style="border:0;display:inline-block" plain="true" form-type="submit" @click="back">
      <van-button round type="primary" size="small" @click="back">返回</van-button>
    </button>
    <button style="border:0;display:inline-block" plain="true" form-type="submit" @click="watchGame">
      <van-button round type="danger" size="small">{{watchText}}</van-button>
    </button>
    <button style="border:0;display:inline-block" plain="true" form-type="submit" open-type="share">
      <van-button round type="info" size="small">分享</van-button>
    </button>
  </form>
  ```

  style 目的为了去除原生 button 的背景，边框啥的，把 button 当一个 div 来使用，然后在 button 中设置 form-type 和 click 属性,这样既不影响 formId 的属性，也对原有逻辑不产生任何影响。之所以将 click 放在 button 上是因为 button 内部的组件没法获取到点击事件。

  formSubmit 代码如下：

  ```javascript
  formSubmit(e) {
    let item = {
      value: e.mp.detail.formId,
      expireTime: Date.now() + 7 * 24 * 60 * 60 * 1000
    };
    this.globalData.formIdList.push(item);
  },
  ```

  该函数是在表单提交时触发，用于获取 formId,将 formId 和这个 formId 的过期时间一起存到 globalData 全局数据中。然后找一个时机将这些 formId 发送给服务器保存起来就行了。

  这里放上我的做法以供参考。

  我是在每次发送 http 请求前检查是不是有 formId 需要发送到服务器，如果有就将这些数据 JSON 序列化后放到一个自定义 header 中，发送出去，具体代码如下(http 请求工具为：flyio)：

  ```javascript
  var Fly = require("flyio/dist/npm/wx");
  var fly = new Fly();
  //在请求预处理中
  fly.interceptors.request.use(request => {
    request.headers["jwt-token"] = wxUtil.getGlobalData("jwt-token");
    //如果有formId就放到header里送过去
    let formIdList = getApp().globalData.formIdList;
    if (formIdList.length > 0) {
      request.headers["formIdList"] = JSON.stringify(formIdList);
      getApp().globalData.formIdList = [];
    }
    if (request.method == "GET") {
      request.params["_t"] = new Date().getTime();
    }
    return request;
  });
  ```

  下面将后台的实现，基于 Spring Boot.

  # 后台处理

  ## 搜集 formId

  首先需要把 formId 收集起来存到数据库，那么就需要检查每个请求，看 header 中有没有携带 formId,如果有就存到数据库中，注意要和用户对应起来，某个用户点击产生的 formId 只能用于给这个用户推送消息。

  因为要将 formId 和用户绑定起来，因此我是在身份认证过滤器中进行的 formId 处理，身份认证成功后，处理 formId。代码如下：

  ```java
  /**
   * Description: 从请求头中获取formIdList,并插入数据库
   *
   * @param request 请求头
   * @return void
   * @author fanxb
   * @date 2019/5/6 16:39
   */
  private void checkFormId(HttpServletRequest request) {
      String str = request.getHeader(Constant.HEADER_FORM_ID);
      if (StringUtil.isEmpty(str)) {
          return;
      }
      List<FormKey> formKeyList = JSON.parseArray(str, FormKey.class);
      //UserContextHolder用户将当前线程和用户绑定起来，方便后面获取用户信息
      int userId = UserContextHolder.get().getUser().getUserId();
      formKeyList.forEach(item -> item.setUserId(userId));
      this.formKeyDao.insertMany(formKeyList);
  }
  ```

  ## 发送微信提醒

  通过官方文档可以知道发送消息的流程如下：

  1 获取 accessToken,调用微信的大多数接口都需要这个东西，这个通过 appId 和 secret 来获取。详情参见：[https://developers.weixin.qq.com/miniprogram/dev/api-backend/auth.getAccessToken.html](https://developers.weixin.qq.com/miniprogram/dev/api-backend/auth.getAccessToken.html)

  2 调用微信发送服务通知的接口.这个接口文档在:[点击跳转](https://developers.weixin.qq.com/miniprogram/dev/api-backend/templateMessage.send.html)

  最终发送的 http 请求是这样的：

  ```javascript
  url: https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=上面获取的accessToken
  method: post

  请求体格式为：application/json; charset=utf-8

  body:{
    "touser": "用户的openId",
    "template_id": "模板id",
    "page": "点击跳转的小程序url路径",
    "form_id": "收集到的formId",
    "data": {
      "keyword1": {
        "value": "您有一个信息的提示消息"
      },
      "keyword2": {
        "value": "这是消息内容"
      }
    },
    "emphasis_keyword": "keyword1.DATA"
  }
  ```

  data 中的数据的按照顺序 keyword1,keyword2 对应于模板中字段的顺序。

  # 结束

  工程全部源码在这里：[github](https://github.com/FleyX/psnDiscountAssistant)

  **PS**
  这就是那个小程序，欢迎批评指正。

  ![小程序](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190430103928.png)

