微信推送公众号模版消息通知 -- machao
业务场景:
某一天,小明开车出去接老婆的机,路上不小心碰撞到了其他的车,于是他通过车险小程序把事故现场上传给理赔人员处理.
过了一段时间,经过理赔人员的审核后,觉得赔付没有问题,需要发送案件进度通知给小明.
这时候,理赔人员问他们的小程序的开发人员,我们应该选择那种消息通知策略比较合理呢?
消息通知策略:
1. 小程序模版消息
模板推送位置:服务通知
模板下发条件:用户本人在微信体系内与页面有交互行为后触发,包括: 支付,提交表单
模板跳转能力:点击查看详情,仅能跳转 下发模板的小程序的各个页面
2. 公众号模版消息
模板推送位置:公众号
模版下发能力: 服务端可主动下发消息
模板跳转能力:点击查看详情,能跳转 下发模板消息的公众号是绑定关联关系的小程序的各个页面
3. 小程序模版消息效果与公众号模版消息效果对比:
分析:
通过小程序模版消息推送, 消息会被推送到“服务通知”栏目中,“服务通知”栏目中会存在很多其他小程序的推送,这样看起来会很杂乱.
另外,小程序模版消息推送前提, 必须要在用户本人在微信体系内与页面有交互行为后触发, 即不能延迟推送. 当然也可以通过保存交互过程中的fromid达到延迟推送的效果, 但是这个消息只能推送给触发这个交互行为的用户, 这样会导致另外一个问题: 假如理赔人员除了希望推送案件进度通知给小明外,还希望把消息推送给上级领导,那这个就做不到了.
因此,推送公众号模版消息才是最便捷的策略.
如何实现微信小程序推送公众号模版消息?
首先我们需要清楚以下几点:
1. 消息发给谁? -- who?
2. 消息怎么发? -- how?
3. 消息内容发什么? -- what?
对于第一点(who?)
由于我们的主体是小程序, 因此我们没有办法直接通过用户的小程序openid直接进行公众号模版消息的发送, 这时候上面的unionId机制就起了至关重要的作用了.
UnionID机制
我们可以通过将小程序和公众号挂载在同一个微信开放平台帐号下, 通过unionId进行逻辑关联, 这时候我们就可以通过用户的小程序openid找到用户的公众号openid,进而进行公众号的模版消息推送.
由于开发者经常有需在多个平台(移动应用、网站、公众帐号)之间共通用户帐号,统一帐号体系的需求,微信开放平台提供了UnionID机制。
换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID是相同的。
微信开放平台帐号下的不同应用包括: 小程序, 公众号, 移动应用等.
对于第二点(how?)
小程序的模版消息有自己的推送模版消息的api接口, 公众号的模版消息也有自己的推送模版消息的api接口. 为了便捷管理优化, 于是微信api推出一个“统一服务消息”接口, 我们接下来也将使用这个接口进行消息的下发.
统一服务消息
对于第三点(what?)
这里当然就是发送模版消息啦, 需要到 公众号-模版消息 新增自己的模版.
思路:
- 将小程序和公众号挂载在同一个开发平台账号下,这样就能多个不同的主体共用一个相同的UnionID啦.
- 在用户登录小程序时, 通过小程序的获取用户信息接口, 得到并保存用户的小程序openid + unionId.
- 同时引导用户在小程序内通过公众号网页授权, 从而得到并保存用户的公众号openid + unionId.
- 小程序用户通过unionId关联找到公众号openid,进而下发模版消息.
准备:
- 公众号的appid+ secret
小程序的appid+ secret- 前往微信开放平台-管理中心,将公众号,小程序都绑定在同一个开放平台中
- 认证服务器为公众号开发者[只需认证一次]
a. 进入微信公众平台,登录公众号账号
b. 开发-基本配置, 填写服务器配置
填写服务器配置
c. 验证服务器地址的有效性
公众号开发者认证服务器详细步骤
认证服务器为公众号开发者详细步骤:
- 添加开发者服务器认证接口
@Api("微信公众号开发者API接口") @Controller @RequestMapping("/wxPublic/serverApi") public class WxServerApi { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); //在 微信公众号-服务器配置 中配置, 对应“令牌(Token)” @Value("${wx.public.wxServerAuthenToken}") String wxServerAuthenToken; /** * 开发者认证接口 * 微信api用于认证 服务器 是否为可用服务器 * @param signature * @param timestamp * @param nonce * @param echostr * @return */ @ApiOperation(value = "开发者认证接口", notes = "开发者认证接口") @GetMapping("/wxAuthenConfig") @ResponseBody public String wxAuthenConfig(String signature, String timestamp, String nonce, String echostr) { LOGGER.info("开发者认证接口 - 开始签名验证:" + " PARAM VAL: >>>" + signature + "\t" + timestamp + "\t" + nonce + "\t" + echostr); if (StringUtils.isNotEmpty(signature) && StringUtils.isNotEmpty(timestamp) && StringUtils.isNotEmpty(nonce) && StringUtils.isNotEmpty(echostr)) { String sTempStr = ""; try { sTempStr = SHA1.getSHA1(timestamp, nonce, wxServerAuthenToken, ""); } catch (Exception e) { e.printStackTrace(); } if (StringUtils.isNotEmpty(sTempStr) && StringUtils.equals(signature, sTempStr)) { LOGGER.info("开发者认证接口 - 开始签名验证 - 验证成功:-----------:" + sTempStr); return echostr; } else { LOGGER.info("开发者认证接口 - 开始签名验证 - 验证失败:-----------:00000"); return "-1"; } } else { LOGGER.info("开发者认证接口 - 开始签名验证 - 验证失败:-----------:11111"); return "-1"; } } }
/** * SHA1 class * * 计算公众平台的消息签名接口. */ public class SHA1 { /** * 用SHA1算法生成安全签名 * @param token 票据 * @param timestamp 时间戳 * @param nonce 随机字符串 * @param encrypt 密文 * @return 安全签名 * @throws AesException */ public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException { try { String[] array = new String[] { token, timestamp, nonce, encrypt }; StringBuffer sb = new StringBuffer(); // 字符串排序 Arrays.sort(array); for (int i = 0; i < 4; i++) { sb.append(array[i]); } String str = sb.toString(); // SHA1签名生成 MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(str.getBytes()); byte[] digest = md.digest(); StringBuffer hexstr = new StringBuffer(); String shaHex = ""; for (int i = 0; i < digest.length; i++) { shaHex = Integer.toHexString(digest[i] & 0xFF); if (shaHex.length() < 2) { hexstr.append(0); } hexstr.append(shaHex); } return hexstr.toString(); } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.ComputeSignatureError); } } }
- 在开发-基本配置, 填写服务器配置
“服务器地址(URL)” 填写 认证接口的地址
“令牌(Token)” 填写 代码中的wxServerAuthenToken的值
"消息加解密方式" 选择 明文- 提交修改后, 微信会请求访问接口, 这样认证就完成了.
在用户登录小程序时, 通过小程序的获取用户信息接口,获得用户的小程序openid + unionId
步骤
- 小程序通过wx.login()获得 code
wx.login({ success(res){ let code=res.code }, fail(res){ that.$message("微信登录失败,请退出重试!") } })
- 服务后端通过code + 小程序appid + 小程序secret, 请求 auth.code2Session API接口得到用户的小程序openid + unionId
/** * 微信小程序openid工作类 * @author mac * */ @SuppressWarnings("deprecation") @Component public class WxOpenidUtil { private Logger LOGGER = >LoggerFactory.getLogger(this.getClass()); final String openidUrl = "https://api.weixin.qq.com/sns/jscode2session"; @Value("${wx.applet.appid}") String wxAppid; @Value("${wx.applet.secret}") String wxSecret; @Value("${wx.applet.grantType}") String grantType; @SuppressWarnings("resource") public WxOpenIdPo GetWxOpenId(String wxcode) { WxOpenIdPo info = null; // 微信API接口 String url = openidUrl + "?appid=" + wxAppid + "&secret=" + wxSecret + "&js_code=" + wxcode + "&grant_type=" + grantType + ""; HttpGet request = new HttpGet(url); HttpResponse response = null; try { HttpClient client = new DefaultHttpClient(); response = client.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String strResult = EntityUtils.toString(response.getEntity()); if (!StringUtils.isEmpty(strResult)) { info = JacksonUtil.defaultInstance().json2pojo(strResult, WxOpenIdPo.class); } } } catch (IOException e) { e.printStackTrace(); LOGGER.error("根据wxcode获取微信小程序openid失败: wxcode(" + wxcode + ")"); } return info; } }
- 将获取到的 用户的小程序openid + unionId保存
引导用户在小程序内通过公众号网页授权, 从而得到并保存用户的公众号openid + unionId.
步骤:
- 服务后端添加“公众号授权网页url”接口, 返回“公众号授权网页url“
- 服务后端添加回调接口, 用于微信api回调, 接口会携带用户的公众号code(划重点)
- 小程序内通过web-view标签访问“公众号授权网页url”接口返回的url, 引导用户公众号授权, 授权通过后, 微信api会回调“公众号授权网页url”提供的回调接口
- 在回调接口接口中, 通过微信api携带的用户公众号code, 获取网页授权access_token+用户的公众号openid
(我们这里
把 [网页授权access_token] 叫为 [Oauth2AccessToken],
把 [基础支持中的access_token] 叫为 [PublicAccessToken] ,
这样大家容易理解
)- 这时候,我们已经得到用户的公众号openid了,但是没有用户的unionId,没有办法与用户的小程序openid进行逻辑关联. 于是我们还要通过 Oauth2AccessToken + openid 拉取用户信息, 这个接口会返回用户的unionId,这时公众号与小程序的用户正式逻辑关联起来了.
贴代码啦:
- 1.“公众号授权网页url”接口
@Value("${wx.public.appid}") String publicAppid; @Value("${wx.public.secret}") String publicSecret; @Value("${yd.baseHost}") String baseHost;//应用链接 @Value("${wx.public.clientReceiveOpenidUrl}") String clientReceiveOpenidUrl;//接口路径 @Value("${wx.public.clientState}") String clientState;//state, 用于回调接口检验 /** * 返回 微信公众号获取openid的url (并添加回调路径) * @throws UnsupportedEncodingException */ @ApiOperation(value = "返回 微信公众号获取openid的url (并添加回调路径)", notes = "返回 微信公众号获取openid的url (并添加回调路径)") @PostMapping("/return2OpenidUrl") @ResponseBody public String return2OpenidUrl() throws UnsupportedEncodingException{ LOGGER.info("微信公众号OpenId接口 - 返回 微信公众号获取openid的url - 开始"); StringBuffer encodeUrl = new StringBuffer(300); encodeUrl.append(baseHost + clientReceiveOpenidUrl); String redirectUrl = URLEncoder.encode(encodeUrl.toString(), "utf-8"); StringBuffer sb = new StringBuffer(); sb.append("https://open.weixin.qq.com/connect/oauth2/authorize?appid="); sb.append(publicAppid); sb.append("&redirect_uri="); sb.append(redirectUrl); sb.append("&response_type=code&scope=snsapi_userinfo"); sb.append("&state="); sb.append(clientState); LOGGER.info("微信公众号OpenId接口 - 返回 微信公众号获取openid的url - 结束 - redirectUrl("+sb.toString()+")"); return sb.toString(); }
- 服务后端添加回调接口
@Autowired Oauth2AccessTokenBuilder oauth2AccessTokenBuilder; @Autowired SnsapiUserinfoUtil snsapiUserinfoUtil; @Autowired UserMapper userMapper; @Value("${wx.public.appid}") String publicAppid; @Value("${wx.public.secret}") String publicSecret; @Value("${yd.baseHost}") String baseHost; @Value("${wx.public.clientReceiveOpenidUrl}") String clientReceiveOpenidUrl; @Value("${wx.public.clientState}") String clientState; /** * 初始化 公众号用户基本信息(openid+unionid) 接口 * - 微信公众号回调的接口 [携带上 code=CODE&state=STATE] * @param request * @param response * @return */ @GetMapping("/initPublicUserInfo") @ResponseBody public void initPublicUserInfo(@RequestParam("code") String code,@RequestParam("state") String state){ LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - 开始 - code("+code+") state("+state+")"); //校验 重定向携带的state参数 if(!clientState.equals(state)){ LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - state不匹配 - 结束"); return; } //通过code换取网页授权access_token + openid //这里通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同 if(StringUtils.isEmpty(code)){ LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - code为空 - 结束"); return; } Oauth2AccessToken oauth2AccessToken = new Oauth2AccessToken(oauth2AccessTokenBuilder, code); String accessToken = oauth2AccessToken.getAccess_token(); String openid = oauth2AccessToken.getOpenid(); LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - 通过code换取网页授权access_token + openid - accessToken("+accessToken+") openid("+openid+")"); //根据 access_token + openid 拉取用户信息(需scope为 snsapi_userinfo) //得到 unionid + 用户基本信息 SnsapiUserinfo snsapiUserinfo = snsapiUserinfoUtil.getSnsapiUserinfo(accessToken, openid); String unionid = snsapiUserinfo.getUnionid(); //保存 公众号openid User usr = new User(); usr.setUnionid(unionid); User one = userMapper.selectOne(usr); if(one != null){ one.setPublicAccountOpenid(openid); userMapper.updateByPrimaryKey(one); } }
网页授权access_token(Oauth2AccessToken)
/** * 网页OAuth2授权 接口返回值 * @author mac * */ public class Oauth2AccessToken { //网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 private String access_token; //用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID private String openid; public String getAccess_token() { return access_token; } public String getOpenid() { return openid; } /** * 使用构建类 构建 */ public Oauth2AccessToken(Oauth2AccessTokenBuilder builder, String code){ Oauth2AccessTokenBuilder build = builder.build(code); this.access_token = build.accessToken; this.openid = build.openid; } }
网页OAuth2授权 构建类(Oauth2AccessTokenBuilder)
/** * 网页OAuth2授权 构建类 * * @author mac * */ @SuppressWarnings("deprecation") @Component public class Oauth2AccessTokenBuilder { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); @Value("${wx.public.appid}") String publicAppid; @Value("${wx.public.secret}") String publicSecret; //网页OAuth2授权 url final private String oauth2AccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code"; //刷新OAuth2授权 url //final private String oauth2RefreshTokenUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN"; // 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 public String accessToken; // 用户刷新access_token //public String refreshToken; // 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID public String openid; /** * 构建 oauth2AccessTokenBuilder * @param code 微信用户code * @return */ public Oauth2AccessTokenBuilder build(String code) { //不需要重复刷新网页授权token //直接根据code请求得到token+openid即可 Oauth2AccessTokenRespPo po = getAccessToken(code); if(po != null && po.errcode != null && !StringUtils.isEmpty(po.errmsg)){ return null; } this.accessToken = po.access_token; this.openid = po.openid; return this; } /** * 获取网页授权的access_token + openID * * @return */ @SuppressWarnings({ "resource" }) private Oauth2AccessTokenRespPo getAccessToken(String code) { // 获取小程序全局唯一后台接口调用凭据 接口 String url = oauth2AccessTokenUrl; url = url.replace("APPID", publicAppid); url = url.replace("SECRET", publicSecret); url = url.replace("CODE", code); LOGGER.info("网页OAuth2授权 构建类 - 获取网页授权的access_token + openID - url: "+ url); HttpGet request = new HttpGet(url); HttpResponse response = null; Oauth2AccessTokenRespPo po = null; try { HttpClient client = new DefaultHttpClient(); response = client.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String strResult = EntityUtils.toString(response.getEntity()); if (!StringUtils.isEmpty(strResult)) { LOGGER.info("网页OAuth2授权 构建类 - 获取网页授权的access_token + openID - 返回: " + strResult); po = JacksonUtil.defaultInstance().json2pojo(strResult, Oauth2AccessTokenRespPo.class); } } } catch (IOException e) { e.printStackTrace(); LOGGER.error("获取网页授权的access_token + openID失败!!!"); } return po; } /** * 接口返回参数PO * @author mac * */ @JsonIgnoreProperties(ignoreUnknown = true) public static class Oauth2AccessTokenRespPo{ //网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 private String access_token; //access_token接口调用凭证超时时间,单位(秒) private Long expires_in; //用户刷新access_token private String refresh_token; //用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID private String openid; //用户授权的作用域,使用逗号(,)分隔 private String scope; //错误码 - 错误时返回 private Integer errcode; //错误信息 - 错误时返回 private String errmsg; public Oauth2AccessTokenRespPo(){} public String getAccess_token() { return access_token; } public void setAccess_token(String access_token) { this.access_token = access_token; } public Long getExpires_in() { return expires_in; } public void setExpires_in(Long expires_in) { this.expires_in = expires_in; } public String getRefresh_token() { return refresh_token; } public void setRefresh_token(String refresh_token) { this.refresh_token = refresh_token; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public Integer getErrcode() { return errcode; } public void setErrcode(Integer errcode) { this.errcode = errcode; } public String getErrmsg() { return errmsg; } public void setErrmsg(String errmsg) { this.errmsg = errmsg; } } }
微信公众号用户信息Po(SnsapiUserinfo)
/** * 微信公众号用户信息 * @author mac * */ @JsonIgnoreProperties(ignoreUnknown = true) public class SnsapiUserinfo { //用户的唯一标识 private String openid; //用户昵称 private String nickname; //用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 private Integer sex; //用户个人资料填写的省份 private String province; //普通用户个人资料填写的城市 private String city; //国家,如中国为CN private String country; //用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 private String headimgurl; //用户特权信息,json 数组,如微信沃卡用户为(chinaunicom) private List<String> privilege; //只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 private String unionid; private String language; private String errcode; private String errmsg; public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getHeadimgurl() { return headimgurl; } public void setHeadimgurl(String headimgurl) { this.headimgurl = headimgurl; } public List<String> getPrivilege() { return privilege; } public void setPrivilege(List<String> privilege) { this.privilege = privilege; } public String getUnionid() { return unionid; } public void setUnionid(String unionid) { this.unionid = unionid; } public String getErrcode() { return errcode; } public void setErrcode(String errcode) { this.errcode = errcode; } public String getErrmsg() { return errmsg; } public void setErrmsg(String errmsg) { this.errmsg = errmsg; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } }
微信公众号用户信息 工具类(SnsapiUserinfoUtil)
/** * 微信公众号用户信息 工具类 * @author mac * */ @SuppressWarnings("deprecation") @Component public class SnsapiUserinfoUtil { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); final String openidUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN"; /** * 获得 微信公众号用户信息 * @param accessToken * @param openid * @return */ @SuppressWarnings({ "resource" }) public SnsapiUserinfo getSnsapiUserinfo(String accessToken, String openid) { SnsapiUserinfo info = null; if(StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(openid)){ LOGGER.info("微信公众号用户信息 工具类 - 获得 微信公众号用户信息 - accessToken||openid 为空"); return null; } // 微信API接口 String url = openidUrl; url = url.replace("ACCESS_TOKEN", accessToken); url = url.replace("OPENID", openid); LOGGER.info("微信公众号用户信息 工具类 - 获得 微信公众号用户信息 - url: " + url); HttpGet request = new HttpGet(url); HttpResponse response = null; try { HttpClient client = new DefaultHttpClient(); response = client.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String strResult = EntityUtils.toString(response.getEntity()); if (!StringUtils.isEmpty(strResult)) { info = JacksonUtil.defaultInstance().json2pojo(strResult, SnsapiUserinfo.class); } } } catch (IOException e) { e.printStackTrace(); LOGGER.info("微信公众号用户信息 工具类 - 获得 微信公众号用户信息 失败"); } return info; } }
- 小程序内通过web-view标签访问“公众号授权网页url”接口返回的url, 引导用户公众号授权
贴代码啦:
html: <div v-if="needPublicAccountAuth"> <web-view :src="publicAccountAuthUrl" bindmessage="getMessage"></web-view> </div> js: //进入公众号授权页面 authPublicAccount(){ let that = this let param={} wx.request({ url: api.wxPublic.return2OpenidUrl, data: param, method: 'POST', header: { 'content-type': 'application/json' }, success:function (res) { //打开webview that.$data.needPublicAccountAuth=true that.$data.publicAccountAuthUrl = res.data; //console.log(res.data) setTimeout(() => { console.log("公众号授权完成") that.$data.needPublicAccountAuth=false that.$data.publicAccountAuthUrl = '' }, 5000); }, fail:function (res) { wx.showToast({ title:"获得公众号授权URL错误", icon:"none", duration:2000 }) } }) }
这样,我们的小程序用户与公众号用户就逻辑关联起来啦
小程序用户通过unionId关联找到公众号openid,进而下发模版消息.
步骤:
- 通过用户的unionId关联得到用户公众号openid
通过用户公众号openid,下发模版消息(这里使用“统一服务消息接口”)
统一服务消息接口贴代码啦:
- 发送微信统一服务消息工具类
***代码中的ReqPo, RespPo, WeappTemplateMsg等类, 是为了方便与接口交互而封装的(接口使用的是json传输) ***
下发小程序和公众号统一的服务消息
参考: 下发小程序和公众号统一的服务消息/** * 发送微信统一服务消息工具类 * * @author mac * */ @SuppressWarnings("deprecation") @Component public class WechatMessageUtil { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); // 发送模版消息接口 final String sendMessageUrl = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN"; @Value("${wx.applet.appid}") String wxAppid; @Value("${wx.applet.secret}") String wxSecret; @Value("${wx.public.appid}") String publicAppid; @Value("${wx.public.secret}") String publicSecret; @Autowired WechatAccessTokenBuilder builder; /** * 发送模版信息 * * @param reqPo * @return * @throws JsonProcessingException */ @SuppressWarnings({ "resource" }) public RespPo uniformSend(ReqPo reqPo) throws JsonProcessingException { RespPo respPo = null; LOGGER.info("发送公众号模版信息 start"); String accessToken = reqPo.getAccess_token(); String reqPoJson = JacksonUtil.defaultInstance().pojo2json(reqPo); // 获取小程序全局唯一后台接口调用凭据 接口 String url = sendMessageUrl.replace("ACCESS_TOKEN", accessToken); HttpPost request = new HttpPost(url); request.setEntity(new StringEntity(reqPoJson, ContentType.DEFAULT_TEXT.withCharset(Charset.defaultCharset()))); request.setHeader(new BasicHeader("Content-Type", "application/json")); HttpResponse response = null; try { HttpClient client = new DefaultHttpClient(); response = client.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String strResult = EntityUtils.toString(response.getEntity()); if (!StringUtils.isEmpty(strResult)) { LOGGER.info("发送公众号模版信息 - 返回: " + strResult); respPo = JacksonUtil.defaultInstance().json2pojo(strResult, RespPo.class); } } } catch (IOException e) { e.printStackTrace(); LOGGER.error("发送公众号模版信息失败!!!"); } return respPo; } /** * 组装发送模版信息所需的请求参数 * * @return * @throws Exception */ public ReqPo packageReqPo(TmplBase base, String openId) throws Exception { ReqPo reqPo = new ReqPo(); MpTemplateMsg mpTemplateMsg = new MpTemplateMsg(); String accessToken = getAccessToken(); if (StringUtils.isEmpty(accessToken)) { LOGGER.info("公众号主体无法获得有效的accessToken"); return null; } reqPo.setTouser(openId);//用户openid,可以是小程序的openid,也可以是mp_template_msg.appid对应的公众号的openid reqPo.setAccess_token(accessToken); reqPo.setMp_template_msg(mpTemplateMsg); reqPo.setWeapp_template_msg(null); mpTemplateMsg.setAppid(publicAppid);// 公众号appid mpTemplateMsg.setTemplate_id(base.tmplID);// template_id mpTemplateMsg.setData(base.getData());// 公众号模板消息的数据 mpTemplateMsg.setMiniprogram(base.miniprogram);// 公众号模板消息所要跳转的小程序 mpTemplateMsg.setUrl(null);// 公众号模板消息所要跳转的url return reqPo; } /** * 获得access_token * * @return * @throws Exception */ private String getAccessToken() { WechatAccessToken token = new WechatAccessToken(builder); return token.getAccessToken(); } }
/** * 下发小程序和公众号统一的服务消息 请求PO * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html * @author mac * */ public class ReqPo { //接口调用凭证 private String access_token; //用户openid,可以是小程序的openid,也可以是mp_template_msg.appid对应的公众号的openid private String touser; //小程序模板消息相关的信息,可以参考小程序模板消息接口; 有此节点则优先发送小程序模板消息 private WeappTemplateMsg weapp_template_msg; //公众号模板消息相关的信息,可以参考公众号模板消息接口;有此节点并且没有weapp_template_msg节点时,发送公众号模板消息 private MpTemplateMsg mp_template_msg; }
/** * 返回值 PO * @author mac * */ public class RespPo { //错误码 0-成功 private int errcode; //错误信息 private String errmsg; }
/** * 小程序模板消息相关的信息 * @author mac * */ public class WeappTemplateMsg { //小程序模板ID private String template_id; //小程序页面路径 private String page; //小程序模板消息formid private String form_id; //小程序模板数据 private String data; //小程序模板放大关键词 private String emphasis_keyword; }
/** * 公众号模板消息相关的信息 * @author mac * */ public class MpTemplateMsg { //公众号appid,要求与小程序有绑定且同主体 private String appid; //公众号模板id private String template_id; //公众号模板消息所要跳转的url private String url; //公众号模板消息所要跳转的小程序,小程序的必须与公众号具有绑定关系 private Miniprogram miniprogram; //公众号模板消息的数据 private Map<String, DataValue> data; }
/** * 公众号模版消息 - 小程序跳转PO * @author mac * */ public class Miniprogram { private String appid="wx2a8dd229406ae6ae"; private String pagepath; }
- 微信小程序WechatAccessToken生成器
由于 发送微信统一服务消息 , 需要使用到微信小程序的accessToken, 因此这里加上微信小程序WechatAccessToken生成器/** * 小程序access_token * @author mac * */ public class WechatAccessToken { private String accessToken; private Date builderTime; /** * 获得 accessToken * @return */ public String getAccessToken() { return accessToken; } /** * 获得 builderTime * @return */ public Date getBuilderTime() { return builderTime; } /** * 使用 内部构建类 构建 * @param builder * @throws Exception */ public WechatAccessToken(WechatAccessTokenBuilder builder){ WechatAccessTokenBuilder build = builder.build(); if(!StringUtils.isEmpty(build.accessToken)){ this.accessToken = build.accessToken; this.builderTime = build.builderTime; } } }
/** * 微信小程序WechatAccessToken生成器 * @author mac * */ @Component @SuppressWarnings("deprecation") public class WechatAccessTokenBuilder { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); @Value("${wx.applet.appid}") String appletAppid; @Value("${wx.applet.secret}") String appletSecret; //获取access_token接口 final String getTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"; public String accessToken; public Date builderTime; /** * 生成 WechatAccessToken * @return * @throws Exception */ public WechatAccessTokenBuilder build() { if(this.accessToken == null || this.builderTime == null){ this.accessToken = getAccessToken(); this.builderTime = new Date(); return this; } //比较时间 Calendar cal = Calendar.getInstance(); cal.setTime(this.builderTime); cal.add(Calendar.HOUR, 1);//往后1小时 //cal.add(Calendar.MINUTE, 5);//往后5分钟 Date outTimeDate = cal.getTime(); Date nowDate = new Date(); if(outTimeDate.after(nowDate)){ return this; }else{ this.accessToken = getAccessToken(); this.builderTime = new Date(); return this; } } /** * 获取access_token * * @return */ @SuppressWarnings({ "resource"}) private String getAccessToken() { // 获取小程序全局唯一后台接口调用凭据 接口 String url = getTokenUrl.replace("APPID", appletAppid); url = url.replace("APPSECRET", appletSecret); HttpGet request = new HttpGet(url); HttpResponse response = null; AccessTokenRespPo po = null; try { HttpClient client = new DefaultHttpClient(); response = client.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String strResult = EntityUtils.toString(response.getEntity()); if (!StringUtils.isEmpty(strResult)) { LOGGER.info("WechatAccessTokenBuilder.获取access_token.返回: " + strResult); po = JacksonUtil.defaultInstance().json2pojo(strResult, AccessTokenRespPo.class); } } } catch (IOException e) { e.printStackTrace(); LOGGER.error("获取微信小程序的access_token失败!!!"); } if (po != null && !"".equals(po.getAccess_token())) { return po.getAccess_token(); } else { return null; } } /** * 接口返回参数PO * @author mac * */ @JsonIgnoreProperties(ignoreUnknown = true) public static class AccessTokenRespPo { public String access_token; public int expires_in; public int errcode; public String errmsg; public AccessTokenRespPo(){} public String getAccess_token() { return access_token; } public void setAccess_token(String access_token) { this.access_token = access_token; } public int getExpires_in() { return expires_in; } public void setExpires_in(int expires_in) { this.expires_in = expires_in; } public int getErrcode() { return errcode; } public void setErrcode(int errcode) { this.errcode = errcode; } public String getErrmsg() { return errmsg; } public void setErrmsg(String errmsg) { this.errmsg = errmsg; } } }
- 发送模版消息Demo
/** * 公众号模版类 * @author mac * */ public abstract class TmplBase { /** * 模版id */ public String tmplID; /** * 模版参数 */ public Map<String, DataValue> data; /** * 公总号跳转小程序 */ public Miniprogram miniprogram; }
demo:
@Autowired WechatMessageUtil wechatMessageUtil; //小程序appid @Value("${wx.applet.appid}") String appletAppid; //公众号appid @Value("${wx.public.appid}") String publicAppid; @Test public void test() throws Exception { //点击模版消息跳转参数 Miniprogram miniprogram = new Miniprogram(); miniprogram.setAppid(appletAppid);//appid需要线上有版本才能匹配对应的路径 miniprogram.setPagepath("pages/Login");//要跳转的路径+参数 //模版消息 TestTmpl testTmpl = new TestTmpl(new DataValue("1"), new DataValue("2"), new DataValue("3"), new DataValue("4"), miniprogram); ReqPo reqPo = wechatMessageUtil.packageReqPo4Customer(testTmpl, publicAppid); RespPo uniformSend = wechatMessageUtil.uniformSend(reqPo); System.out.println(uniformSend); }
终于码完字啦,呼~
看到这里,如果对你们有用,请鼓励一下笔者吧
第一次码字, 有错误请见谅~~