【发送模板消息简易流程】
1.获取access_token
1.1 获取 AppID 和 AppSecret
1.2 按照微信小程序access_token特性来存储在服务端Redis(有效期7200秒,考虑网络延迟等影响,我们可以设置有效期7100秒)
2.发送模板消息
2.1获取提前存好的formId或prepay_id(formId:submit事件产生一个formId,一次性失效,7天内使用,支付成功产生一个prepay_id,可以使用3次,7天内有效)
2.2填写好模板ID以及相关内容,即可发送
【第一步:获取Token】
access_token 是全局唯一接口调用凭据,开发者调用各接口时都需使用 access_token,请妥善保存。access_token 的存储至少要保留512个字符空间。access_token 的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的 access_token 失效。
公众平台的 API 调用所需的 access_token 的使用及生成方式说明:
- 为了保密 appsecrect,第三方需要一个 access_token 获取和刷新的中控服务器(即我们服务端)。而其他业务逻辑服务器所使用的 access_token 均来自于该中控服务器,不应该各自去刷新,否则会造成 access_token 覆盖而影响业务;
- 目前 access_token 的有效期通过返回的 expires_in 来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新 access_token。在刷新过程中,中控服务器对外输出的依然是老 access_token,此时公众平台后台会保证在刷新短时间内,新老 access_token 都可用,这保证了第三方业务的平滑过渡;
- access_token 的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新 access_token 的接口,这样便于业务服务器在 API 调用获知 access_token 已超时的情况下,可以触发 access_token 的刷新流程。
注意:如果第三方不使用中控服务器,而是选择各个业务逻辑点各自去刷新 access_token,那么就可能会产生冲突,导致服务不稳定。所以我们会把access_token保存在服务器的redis上。
[ 接口地址 ]
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
[ HTTP请求方式 ] GET
[参数说明]
参数 | 必填 | 说明 |
---|---|---|
grant_type | 是 | 获取 access_token 填写 client_credential |
appid | 是 | 第三方用户唯一凭证 |
secret | 是 | 第三方用户唯一凭证密钥,即appsecret |
返回参数说明:
正常情况下,微信会返回下述 JSON 数据包给开发者:
{"access_token": "adasdasdaasdnajbdkaushdiaushasbdkasbdk", "expires_in": 7200}
【 简易代码示例 】
/** * 获取微信小程序的Token */ private String getAccessToken() { String accessToken = stringRedisTemplate.opsForValue().get(RedisKeys.getMiniProgramTokenKey()); if (!StringUtils.isEmpty(accessToken)) { //判断Redis中的accessToken是否过期 return accessToken; } String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + "&appid=" + Config.MINI_PROGRAM_APP_ID + "&secret=" + Config.MINI_PROGRAM_SECRET_KEY; String result = customerHttpsClient.doGet(accessTokenUrl, null, null); //发送Get请求 WeChatAccess access = JSON.parseObject(result, WeChatAccess.class); //响应的正确JSON格式为:{"access_token": "asdasdasdsdsad", "expires_in": 7200},所以这个WeChatAccess类可以兼容这个格式 if (access == null || StringUtils.isEmpty(access.getAccess_token())) { //响应的错误JSON格式为:{"errcode": 40013, "errmsg": "invalid appid"} return null; } accessToken = access.getAccess_token(); //利用redis设置保存键的过期时间 stringRedisTemplate.opsForValue().set(KeyUtils.getMiniProgramTokenKey(), accessToken, 7100, TimeUnit.SECONDS); return accessToken; }
【第二步:发送模板消息】
[ 接口地址:(ACCESS_TOKEN 需换成上文获取到的 access_token) ]
https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
[ HTTP请求方式 ] POST
[ POST参数说明 ]
参数 | 必填 | 说明 |
---|---|---|
touser | 是 | 接收者(用户)的 openid |
template_id | 是 | 所需下发的模板消息的id |
page | 否 | 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 |
form_id | 是 | 表单提交场景下,为 submit 事件带上的 formId;支付场景下,为本次支付的 prepay_id |
data | 是 | 模板内容,不填则下发空模板 |
color | 否 | 模板内容字体的颜色,不填默认黑色 |
emphasis_keyword | 否 | 模板需要放大的关键词,不填则默认无放大 |
[ POST请求body示例 ]
{ "touser": "OPENID", "template_id": "TEMPLATE_ID", "page": "index", "form_id": "FORMID", "data": { "keyword1": { "value": "339208499", "color": "#173177" }, "keyword2": { "value": "2015年01月05日 12:30", "color": "#173177" }, "keyword3": { "value": "粤海喜来登酒店", "color": "#173177" } , "keyword4": { "value": "广州市天河区天河路208号", "color": "#173177" } }, "emphasis_keyword": "keyword1.DATA" }
[ 返回码说明 ]
在调用模板消息接口后,会返回JSON数据包。
正常时的返回JSON数据包示例:
{ "errcode": 0, "errmsg": "ok" }
[ 错误时会返回错误码信息 ]
返回码 | 说明 |
---|---|
40037 | template_id不正确 |
41028 | form_id不正确,或者过期 |
41029 | form_id已被使用 |
41030 | page不正确 |
45009 | 接口调用超过限额(目前默认每个帐号日调用限额为100万) |
【注意!!!】
-
支付
当用户在小程序内完成过支付行为(需要支付成功哦),可允许开发者向用户在7天内推送有限条数的模板消息(1次支付可下发3条,多次支付下发条数独立,互相不影响)
-
提交表单
当用户在小程序内发生过提交表单行为且该表单声明为要发模板消息的,开发者需要向用户提供服务时,可允许开发者向用户在7天内推送有限条数的模板消息(1次提交表单可下发1条,多次提交下发条数独立,相互不影响)
参考网址:https://mp.weixin.qq.com/debug/wxadoc/dev/api/notice.html#%E5%8F%91%E9%80%81%E6%A8%A1%E6%9D%BF%E6%B6%88%E6%81%AF
【关于form_Id、prepay_id的处理方式】
1.提供接口给前端,提前准备好发送模板消息需要的“弹药”存到redis中
2.发送模板消息时候,就从Redis中取出对应的form_id/prepay_id
根据form_id、prepay_id的不同特性,使用ZSet格式来存储。
//写一个接口给前端保存对应的form_id或prepay_id存储起来,当支付或者前端来提前储备好对应的“弹药” public Result saveBizIdByOpenIdForMiniProgramMessage(String openId, String bizId, Long expire,Integer count) { if(null == expire){ expire = 7 * 24 * 60 * 60L; //一周的有效期 } MiniProgramMessageBase miniProgramMessageBase = new MiniProgramMessageBase(bizId, expire, System.currentTimeMillis() / 1000); String jsonStr = JSONObject.toJSONString(miniProgramMessageBase); stringRedisTemplate.opsForZSet().add(RedisKeyUtils.getBizIdCountForTemplateMessage(openId), jsonStr.toString(), count); //使用Redis的Zset记录次数 miniProgramMessageBase.setCount(count); return new Result(miniProgramMessageBase); } class MiniProgramMessageBase implements Serializable{ private String bizId; //发送消息模板前保存的formId(提交表单给的)、prepay_id(统一下单给的) private Long expire; //过期所需时间 private Long nowTime; //存入的时间 private Integer count; //可使用次数 public MiniProgramMessageBase(String bizId, Integer count, Long expire, Long nowTime) { this.bizId = bizId; this.count = count; this.expire = expire; this.nowTime = nowTime; } public MiniProgramMessageBase(String bizId, Long expire, Long nowTime) { this.bizId = bizId; this.expire = expire; this.nowTime = nowTime; } public MiniProgramMessageBase() { } //忽略get/set方法.... }
获取form、prepay_id方法
/** * 从redis中获取一个form_id、prepay_id(这里都叫bizId) */ public MiniProgramMessageBase getBizIdByOpenIdForMiniProgramMessage(String openId) { BoundZSetOperations<String, String> opus = stringRedisTemplate.boundZSetOps(RedisKeyUtils.getBizIdCountForTemplateMessage(openId)); Set<ZSetOperations.TypedTuple<String>> courseItemIdSet = opus.rangeWithScores(0, -1); List<MiniProgramMessageBase> messageBaseList = Lists.newArrayList(); for (ZSetOperations.TypedTuple<String> temp : courseItemIdSet) { //遍历ZSet查询出来的数据 if (temp.getScore().intValue() == 0 ) { //发现次数用完的,从redis删掉 stringRedisTemplate.opsForZSet().remove(RedisKeyUtils.getBizIdCountForTemplateMessage(openId), temp.getValue()); } else { MiniProgramMessageBase miniProgramMessageBase = JSONObject.parseObject(temp.getValue(), MiniProgramMessageBase.class); //如果未过期 if ((miniProgramMessageBase.getExpire() + miniProgramMessageBase.getNowTime() > (System.currentTimeMillis() / 1000))){ miniProgramMessageBase.setCount(temp.getScore().intValue()); messageBaseList.add(miniProgramMessageBase); }else{ //如果已过期,删除 stringRedisTemplate.opsForZSet().remove(RedisKeyUtils.getBizIdCountForTemplateMessage(openId), temp.getValue()); } } } if (CollectionUtil.isNotEmpty(messageBaseList)) { Collections.sort(messageBaseList, new Comparator<MiniProgramMessageBase>() { @Override public int compare(MiniProgramMessageBase o1, MiniProgramMessageBase o2) { return o1.getNowTime().compareTo(o2.getNowTime()); } }); return messageBaseList.get(0); } return null; }
【发送模板消息】
private void sendTemplateMessageForMiniProgram(String templateId, String openId, HashMap<String, MiniProgramMsgContent> bizParams,String page) { try { String url = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=" + this.getAccessTokenForMiniProgram(); //微信小程序规则:formId存在redis中,将可用的取出 MiniProgramMessageBase miniProgramMessageBase = userService.getBizIdByOpenIdForMiniProgramMessage(openId); if(null==miniProgramMessageBase || null == miniProgramMessageBase.getBizId()){ return ; } String formId = miniProgramMessageBase.getBizId(); HashMap<String, Object> params = new HashMap<>(); //这里应该优化下,写一个对应的类类组装发送模板消息需要的参数 params.put("touser", openId); params.put("template_id", templateId); params.put("form_id", formId); params.put("page",page); params.put("data", bizParams); String paramsStr = JSONObject.toJSONString(params); String result = customerHttpsClient.doPostWithJsonBody(url, paramsStr, null); //发送模板消息 JSONObject jsonObject = JSONObject.parseObject(result); userService.reductionCountBizIdByOpenIdForMiniProgramMessage(openId,miniProgramMessageBase); //发送过一次模板消息后,应该将次数-1,或者删除 if (jsonObject.getInteger("errcode") != 0) { LOG.error(String.format("小程序消息发送失败,params:%s,result:%s", params, result)); } } catch (Exception e) { LOG.error(String.format("小程序消息发送失败,templateId:%s,openId:%s,bizParams:%s", templateId, openId, JSONObject.toJSONString(bizParams))); } } //发送过一次模板消息后,应该将次数-1,或者删除 public void reductionCountBizIdByOpenIdForMiniProgramMessage(String openId, MiniProgramMessageBase miniProgramMessageBase) { miniProgramMessageBase.setCount(null); String jsonStr = JSONObject.toJSONString(miniProgramMessageBase); Double score = stringRedisTemplate.opsForZSet().incrementScore(KeyUtils.getBizIdCountForTemplateMessage(openId),jsonStr,-1); if(score==null ||score.intValue()<=0){ stringRedisTemplate.opsForZSet().remove(KeyUtils.getBizIdCountForTemplateMessage(openId), jsonStr); } }