zoukankan      html  css  js  c++  java
  • 微信小程序:模板消息推送实现

    模板消息是基于微信的通知渠道,为开发者提供了可以高效触达用户的模板消息能力,以便实现服务的闭环并提供更佳的体验。

    想推送模板消息,得满足一些前提条件:

    1. 用户在小程序中完成支付后,小程序可以向用户发送模板消息。
    2. 用户在小程序中有提交表单的行为,小程序可以向用户发送模板消息。

    例如:

    1. 用户在小程序里购买了商品,小程序可以将商品物流的情况,实时发送给用户。
    2. 用户在小程序里填写了活动报名表后,小程序可以将报名情况(成功或失败)推送给用户。
    需要注意的是,即使条件达成了,小程序也不能无限制地发送模板消息。(貌似一次支付可以推送五次消息通知)

    具体的发送数量限制是:

    1. 用户完成一次支付,小程序可以获得 3 次发送模板消息的机会。
    2. 用户提交一次表单,小程序可以获得 1 次发送模板消息的机会。
    3. 发送模板消息的机会在用户完成操作后的 7 天内有效。一旦超过 7 天,这些发送资格将会自动失效。

    前置准备工作

    内网穿透(需要支持80端口、绑定已备案域名SSL证书)用于开发时调试后端接口。

    源码中已提供该工具

    注册小程序账号,同时申请或定制对应的模板消息,拿到模板ID和模板结构备用。

    https://mp.weixin.qq.com/wxop...


    可以选择自行定制模板消息格式,但是最终需要微信审核后方可使用,这里我们测试,就随意在模板库中挑选了一款,最终得到模板消息格式如下:

    购买地点 {{keyword1.DATA}}
    购买时间 {{keyword2.DATA}}
    物品名称 {{keyword3.DATA}}
    交易单号 {{keyword4.DATA}}

    配置可信服务器域名

    此处的可信域名,最终为内网穿透映射的域名,用于小程序向本地后端接口发送HTTP请求。

    相关的微信API

    获取AccessToken [GET]

    参数是否必须说明
    grant_type 获取access_token填写client_credential
    appid 第三方用户唯一凭证
    secret 第三方用户唯一凭证密钥,即appsecret

    正常情况下,微信会返回下述JSON数据包给公众号:

    {"access_token":"ACCESS_TOKEN","expires_in":7200}

    登录凭证校验: 根据js_code换取当前用户的openId [GET]

    先通过小程序获取当前用户的js_code,再调用相关接口接口换取openId

    wx.login(OBJECT)

    调用接口wx.login() 获取临时登录凭证(js_code)

    wx.login({
      success: function(res) {
        if (res.code) {
          // 获取到js_code, 可继续调用接口换取openId
        } else {
          console.log('登录失败!' + res.errMsg)
        }
      }
    });

    https://api.weixin.qq.com/sns...{}&secret={}&js_code={}&grant_type=authorization_code

    参数是否必须说明
    appid 小程序唯一标识
    secret 小程序的 app secret
    js_code 登录时获取的 code
    grant_type 填写为 authorization_code
    //正常返回的JSON数据包
    {
        "openid": "OPENID",
        "session_key": "SESSIONKEY",
    }
    
    //满足UnionID返回条件时,返回的JSON数据包
    {
        "openid": "OPENID",
        "session_key": "SESSIONKEY",
        "unionid": "UNIONID"
    }
    //错误时返回JSON数据包(示例为Code无效)
    {
        "errcode": 40029,
        "errmsg": "invalid code"
    }

    发送模板消息 [POST]

    https://api.weixin.qq.com/cgi...

    参数是否必须说明
    touser 接收者(用户)的 openid
    template_id 所需下发的模板消息的id
    page 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。
    form_id 表单提交场景下,为 submit 事件带上的 formId;支付场景下,为本次支付的 prepay_id
    data 模板内容,不填则下发空模板
    emphasis_keyword 模板需要放大的关键词,不填则默认无放大

    请求示例:

    {
      "touser": "OPENID",
      "template_id": "TEMPLATE_ID",
      "page": "index",
      "form_id": "FORMID",
      "data": {
          "keyword1": {
              "value": "339208499"
          },
          "keyword2": {
              "value": "2015年01月05日 12:30"
          },
          "keyword3": {
              "value": "粤海喜来登酒店"
          } ,
          "keyword4": {
              "value": "广州市天河区天河路208号"
          }
      },
      "emphasis_keyword": "keyword1.DATA"
    }

    代码实现

    注意:下面的代码均为测试代码,未考虑严谨性,仅为实现功能。

    小程序端

    <!--index.wxml-->
    <view class="container">
      <view class="userinfo">
        <button wx:if="{{!hasUserInfo || !hasOpenId}}"
                open-type="getUserInfo"
                bindgetuserinfo="getUserInfo"
                type='primary'
                size='mini'>获取用户信息</button>
        <block wx:else>
          <image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
          <text class="userinfo-nickname">{{userInfo.nickName}}</text>
          <text class="userinfo-nickname">{{openId}}</text>
        </block>
      </view>
      <view wx:if="{{hasUserInfo && hasOpenId}}" class='usermotto'>
        <form bindsubmit="templateSend" report-submit="true">
          <button type='primary' formType="submit" size='mini'>发送模板消息</button>
        </form>
      </view>
      <view wx:if="{{logMessage}}">
        <p style="color:red"><span>{{logMessage}}</span></p>
      </view>
    </view>

    需要注意的是,这里的表单需要加上report-submit="true"属性,标识该属性表示可以获得一次formId的机会,该formId可以用来推送模板消息,下面是控制器相关的代码:

    //index.js
    //获取应用实例
    const app = getApp();
    const requestHost = "https://wuwz.guyubao.com/wx_small_app";
    
    Page({
      data: {
        userInfo: {},
        openId: null,
        hasUserInfo: false,
        hasOpenId: false,
        logMessage: null
      },
      getUserInfo: function(e) {
        app.globalData.userInfo = e.detail.userInfo
        this.setData({
          userInfo: e.detail.userInfo,
          hasUserInfo: true,
          logMessage: '加载用户信息中..'
        })
        this.getOpenId();
      },
      getOpenId: function() {
        var _this = this;
        wx.login({
          success: function(res) {
            if (res.code) {
              // 换取openid
              wx.request({
                url: requestHost + "/get_openid_by_js_code",
                data: {
                  js_code: res.code
                },
                method: 'GET',
                success: function(res) {
                  if (res.data.openid) {
                    _this.setData({
                      openId: res.data.openid,
                      hasOpenId: true,
                      logMessage: '加载用户信息完成'
                    });
                  }
                },
                fail: function (err) {
                  _this.setData({
                    logMessage: '[fail]' + JSON.stringify(err)
                  });
                }
              });
            }
          }
        })
      },
      templateSend: function(e) {
        var _this = this;
        var openId = _this.data.openId;
        // 表单需设置report-submit="true"
        var formId = e.detail.formId;
    
        if (!formId || 'the formId is a mock one' === formId) {
          _this.setData({
            logMessage: '[fail]请使用真机调试,否则获取不到formId'
          });
          return;
        }
    
        // 发送随机模板消息
        wx.request({
          url: requestHost + "/template_send",
          data: {
            openId: openId,
            formId: formId
          },
          method: 'POST',
          success: function(res) {
            if (res.data.status === 0) {
              _this.setData({
                logMessage: '发送模板消息成功[' + new Date().getTime()+']'
              });
            }
          },
          fail: function(err) {
            _this.setData({
              logMessage: '[fail]' + JSON.stringify(err)
            });
          }
        });
      }
    })

    后端接口

    先针对需要使用的微信API做一个简单的封装:

    package com.wuwenze.wechatsmallapptmplmsg.wechat;
    
    import cn.hutool.core.util.StrUtil;
    import cn.hutool.http.HttpRequest;
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.cache.LoadingCache;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author wwz
     * @version 1 (2018/8/20)
     * @since Java7
     */
    @Slf4j
    public class WechatApi {
        private final static LoadingCache<String, String> mAccessTokenCache =
                CacheBuilder.newBuilder()
                        .expireAfterWrite(7200, TimeUnit.SECONDS)
                        .build(new CacheLoader<String, String>() {
                            @Override
                            public String load(String key) {
                                // key: appId#appSecret
                                String[] array = key.split("#");
                                if (null == array || array.length != 2) {
                                    throw new IllegalArgumentException("load access_token error, key = " + key);
                                }
                                return getAccessToken(array[0], array[1]);
                            }
                        });
    
        public static String getAccessToken() {
            String cacheKey = WechatConf.appId + "#" + WechatConf.appSecrct;
            try {
                return mAccessTokenCache.get(cacheKey);
            } catch (ExecutionException e) {
                log.error("#getAccessToken error, cacheKey=" + cacheKey, e);
            }
            return null;
        }
    
        private static String getAccessToken(String appId, String appSecret) {
            String apiUrl = StrUtil.format(//
                    "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}",//
                    appId, appSecret
            );
            String body = HttpRequest.get(apiUrl).execute().body();
            return throwErrorMessageIfExists(body).getString("access_token");
        }
    
        public static void templateSend(String accessToken, WechatTemplate template) {
            String apiUrl = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token="//
                    + (StrUtil.isEmpty(accessToken) ? getAccessToken() : accessToken);
            String body = HttpRequest.post(apiUrl).body(JSON.toJSONString(template)).execute().body();
            throwErrorMessageIfExists(body);
        }
    
        public static JSONObject getOpenIdByJSCode(String js_code) {
            String apiUrl = StrUtil.format(//
                    "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",//
                    WechatConf.appId, WechatConf.appSecrct, js_code
            );
            String body = HttpRequest.get(apiUrl).execute().body();
            return throwErrorMessageIfExists(body);
        }
    
        private static JSONObject throwErrorMessageIfExists(String body) {
            String callMethodName = (new Throwable()).getStackTrace()[1].getMethodName();
            log.info("#0820 {} body={}", callMethodName, body);
            JSONObject jsonObject = JSON.parseObject(body);
            if (jsonObject.containsKey("errcode") && jsonObject.getIntValue("errcode") > 0) {
                throw new RuntimeException(StrUtil.format("#WechatApi[{}] call error: {}", callMethodName, body));
            }
            return jsonObject;
        }
    }

    对外开放相关的接口:

    package com.wuwenze.wechatsmallapptmplmsg.controller;
    
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.RandomUtil;
    import cn.hutool.core.util.StrUtil;
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.wuwenze.wechatsmallapptmplmsg.util.MapUtil;
    import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatApi;
    import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatConf;
    import com.wuwenze.wechatsmallapptmplmsg.util.SecurityUtil;
    import com.wuwenze.wechatsmallapptmplmsg.util.WebUtil;
    import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatTemplate;
    import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatTemplateItem;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.Map;
    import java.util.stream.Stream;
    
    /**
     * @author wwz
     * @version 1 (2018/8/16)
     * @since Java7
     */
    @Slf4j
    @RestController
    @RequestMapping("/wx_small_app")
    public class WechatController {
    
        @GetMapping("/get_openid_by_js_code")
        public Map<String, Object> getOpenIdByJSCode(String js_code) {
            return WechatApi.getOpenIdByJSCode(js_code);
        }
    
        @PostMapping("/template_send")
        public Map<String, Object> templateSend() {
            String accessToken = WechatApi.getAccessToken();
            JSONObject body = JSON.parseObject(WebUtil.getBody());
    
            // 填充模板数据 (测试代码,写死)
            WechatTemplate wechatTemplate = new WechatTemplate()
                    .setTouser(body.getString("openId"))
                    .setTemplate_id(WechatConf.templateId)
                    // 表单提交场景下为formid,支付场景下为prepay_id
                    .setForm_id(body.getString("formId"))
                    // 跳转页面
                    .setPage("index")
                    /**
                     * 模板内容填充:随机字符
                     * 购买地点 {{keyword1.DATA}}
                     * 购买时间 {{keyword2.DATA}}
                     * 物品名称 {{keyword3.DATA}}
                     * 交易单号 {{keyword4.DATA}}
                     * -> {"keyword1": {"value":"xxx"}, "keyword2": ...}
                     */
                    .setData(MapUtil.newHashMap(//
                            "keyword1", new WechatTemplateItem(RandomUtil.randomString(10)),//
                            "keyword2", new WechatTemplateItem(DateUtil.now()),//
                            "keyword3", new WechatTemplateItem(RandomUtil.randomString(10)),//
                            "keyword4", new WechatTemplateItem(RandomUtil.randomNumbers(10)) //
                    ));
            WechatApi.templateSend(accessToken, wechatTemplate);
            return MapUtil.newHashMap("status", 0);
        }
    
        @GetMapping("/validate")
        public void validate(String signature, String timestamp, String nonce, String echostr) {
            final StringBuilder attrs = new StringBuilder();
            Stream.of(WechatConf.token, timestamp, nonce)//
                    .sorted()//
                    .forEach((item) -> attrs.append(item));
            String sha1 = SecurityUtil.getSha1(attrs.toString());
            if (StrUtil.equalsIgnoreCase(sha1, signature)) {
                WebUtil.write(echostr);
                return;
            }
            log.error("#0820 WechatController.validate() error, attrs = {}", attrs);
        }
    }

    其他:突破发送模板消息的限制

    如非必要,尽量不要这样做,一旦发现小程序滥用模板消息,微信是有权进行封禁的。

    简单来说,我们可以将小程序的表单组件进行封装,伪装小程序中其他功能按钮。当用户点击按钮时,表单组件就自动把formId上传给服务器保存(7天后过期),当收集到一定的用户点击事件后,就可以拿来使用了(主动消息推送群发),哈哈哈。

    原文地址:https://segmentfault.com/a/1190000016183735?utm_source=tag-newest

  • 相关阅读:
    Nginx的配置文件详解(超详细)
    淘宝地址爬取及UI展示
    点击观看
    winform picturebox设置布局样式
    vs的一个不经常用的快捷键
    C# 开发windows服务
    winform窗体置顶
    C# mysql set和enum属性字段的读取和添加
    winform窗体绑定监控键盘事件
    html5创建的sqlite存放为止以及在手机中的位置
  • 原文地址:https://www.cnblogs.com/xiaobug/p/11490683.html
Copyright © 2011-2022 走看看