因为微信开发需要配置服务器域名,所以需要一个已备案的域名。可以通过
NPS
内网穿透工具的增强功能,使用
https
将域名的
https
请求穿透到本地网关端口。
NPS
使用:
NPS内网穿透-CSDN博客
文末附有后端服务代码。
1. 小程序消息推送
1.1 功能介绍
消息能力是小程序能力中的重要组成,我们为开发者提供了订阅消息能力,以便实现服务的闭环和更优的体验。
订阅消息推送位置:服务通知
订阅消息下发条件:开发者通过一定的方式触发用户主动订阅
订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面
简单来讲,用户通过点击或支付完成后调用模板消息授权的弹窗,当用户成功授权后,开发者才可以通过调用微信接口,使用用户已授权模板发送订阅消息,进行消息提醒。
1.2 前提条件
一个微信小程序账号,一个已备案域名。
1.3 准备工作
1.3.1 开通订阅消息
登录小程序账号后,点击功能下的订阅消息,点击开通。
1.3.2 配置服务器域名
登录小程序账号后,点击开发的开发管理,在开发设置下,找到服务器域名模块,修改request
合法域名为 自己已备案域名。
1.3.3 生成小程序密钥
若没有生成小程序密钥,则需要在开发设置下,生成小程序密钥。
1.4 订阅消息
1.4.1 一次性订阅
一次性订阅消息指的是:用户订阅模板消息一次,只能推送一条模板消息。当第一次推送过后,用户尚未进行下一次的订阅操作,再次推送就会提示 用户未订阅消息 的错误信息。
1)获取并保存用户openid
获取用户openid
主要为两步,小程序获取用户登录凭证返回至后端服务,后端服务通过登录凭证获取用户的openid
。
① 获取登录凭证
小程序通过调用登录开放接口wx.login()
获取到用户登陆凭证code,发送到自己的后端服务。
详情请见: 微信官方文档-wx.login
参数:
Object object
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
timeout | number | 否 | 超时时间,单位ms | |
success | function | 否 | 接口调用成功的回调函数 | |
fail | function | 否 | 接口调用失败的回调函数 | |
complete | function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
object.success 回调函数
参数:
Object res
属性 | 类型 | 说明 |
---|---|---|
code | string | 用户登录凭证(有效期五分钟)。 |
object.fail 回调函数
参数:
Object err
属性 | 类型 | 说明 | 最低版本 |
---|---|---|---|
errMsg | String | 错误信息 | |
errno | Number | errno 错误码,错误码的详细说明参考 Errno错误码 | 2.24.0 |
示例代码:
wx.login({
success (res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://example.com/wx/login',
data: {
code: res.code,
userId:"001"
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
② 获取用户openid
后端服务通过调用 code2Session
接口,获取到用户openid
详情请见: 微信官方文档-小程序登录
接口文档:
GET https://api.weixin.qq.com/sns/jscode2session
请求参数:
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
appid | string | 是 | 小程序 appId |
secret | string | 是 | 小程序 appSecret |
js_code | string | 是 | 登录时获取的 code |
grant_type | string | 是 | 授权类型,此处只需填写 authorization_code |
返回参数
属性 | 类型 | 说明 |
---|---|---|
session_key | string | 会话密钥 |
unionid | string | 用户在开放平台的唯一标识符 |
errmsg | string | 错误信息 |
openid | string | 用户唯一标识 |
errcode | int32 | 错误码 |
错误码:
错误码 | 错误码取值 | 解决方案 |
---|---|---|
40029 | code 无效 | js_code 无效 |
45011 | api minute-quota reach limit mustslower retry next minute | API 调用太频繁,请稍候再试 |
40226 | code blocked | 高风险等级用户,小程序登录拦截 。风险等级详见用户安全解方案 |
-1 | system error | 系统繁忙,此时请开发者稍候再试 |
示例代码:
private final String appletAppid = "小程序的appid";
private final String appletSecret = "小程序的appSecret";
public void getAppletOpenid(String code) {
RestTemplate restTemplate = new RestTemplate();
// 调用 code2Session 接口
String codeJson = restTemplate.getForObject("https://api.weixin.qq.com/sns/jscode2session" +
"?appid=" + appletAppid +
"&secret=" + appletSecret +
"&js_code=" + code +
"&grant_type=authorization_code", String.class);
// 反序列化
Map bean = JSONUtil.parseObj(codeJson).toBean(Map.class);
if (bean.get("openid") != null){
saveOpenid(bean.get("openid").toString());
}else {
throw new RuntimeException("openid获取失败, 错误码:" + bean.get("errcode") + " 错误信息:" + bean.get("errmsg"));
}
}
private void saveOpenid(String openid){
// 可以从token中获取到用户信息,并将openid保存至数据库
System.out.println("用户的openid:" + openid + " 获取成功");
}
2)用户授权
这里我们首先需要选择需要用户授权的模板,然后让用户调起订阅界面。
① 选用模板
为了简单测试,这里选用了公共模板库中的一个下单提醒模板。若为了满足业务需求,可以申请一个模板(每月有申请次数限制!)。
如果公共模板库没有模板,需先在小程序设置中,选择服务类目。
申请模板在公共模板库的最后一页帮忙我们完善模板库,点击进去,完善相关信息,等待审核。若审核通过,就可以使用了。
② 用户调用
通过用户点击调起客户端小程序订阅消息模板界面,返回用户订阅消息模板的操作结果。当用户勾选了"总是保持以上选择,不再询问",在下次授权时,默认确定,不再进行弹窗提示(真机调试才会出现)。
详情请见: 微信官方文档-wx.requestSubscribeMessage
接口:
wx.requestSubscribeMessage(Object object)
参数:
Object object
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
tmplIds | Array | 是 | 需要订阅的消息模板的id的集合,一次调用最多可订阅3条消息(注意:iOS 客户端7.0.6版本、Android客户端7.0.7版本之后的一次性订阅/长期订阅才支持多个模板消息,iOS 客户端7.0.5版本、Android客户端7.0.6版本之前的一次订阅只支持一个模板消息)消息模板id在[微信公众平台(mp.weixin.qq.com )-功能-订阅消息]中配置。每个tmplId 对应的模板标题需要不相同,否则会被过滤。 | |
success | function | 否 | 接口调用成功的回调函数 | |
fail | function | 否 | 接口调用失败的回调函数 | |
complete | function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
object.success 回调函数
参数:
Object res
属性 | 类型 | 说明 |
---|---|---|
errMsg | String | 接口调用成功时errMsg 值为’requestSubscribeMessage :ok ’ |
[TEMPLATE_ID: string] | String | [TEMPLATE_ID]是动态的键,即模板id,值包括’accept’、‘reject’、‘ban’、‘filter’。'accept’表示用户同意订阅该条id对应的模板消息,'reject’表示用户拒绝订阅该条id对应的模板消息,'ban’表示已被后台封禁,'filter’表示该模板因为模板标题同名被后台过滤。例如 { errMsg : “requestSubscribeMessage :ok”, zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE : “accept”} 表示用户同意订阅zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE 这条消息 |
object.fail 回调函数
参数:
Object res
属性 | 类型 | 说明 |
---|---|---|
errMsg | String | 接口调用失败错误信息 |
errCode | Number | 接口调用失败错误码 |
错误码:
errCode | errMsg | 说明 |
---|---|---|
10001 | TmplIds can't be empty | 参数传空了 |
10002 | Request list fail | 网络问题,请求消息列表失败 |
10003 | Request subscribe fail | 网络问题,订阅请求发送失败 |
10004 | Invalid template id | 参数类型错误 |
10005 | Cannot show subscribe message UI | 无法展示 UI ,一般是小程序这个时候退后台了导致的 |
20001 | No template data return, verify the template id exist | 没有模板数据,一般是模板 ID 不存在 或者和模板类型不对应 导致的 |
20002 | Templates type must be same | 模板消息类型 既有一次性的又有永久的 |
20003 | Templates count out of max bounds | 模板消息数量超过上限 |
20004 | The main switch is switched off | 用户关闭了主开关,无法进行订阅 |
20005 | This mini program was banned from subscribing messages | 小程序被禁封 |
20013 | Reject DeviceMsg Template | 不允许通过该接口订阅设备消息 |
示例代码:
var TemplateId = '第一步选用的模板Id'
var TemplateStatus = wx.getStorageSync('TemplateID')
if(TemplateStatus != 'accept'){
wx.requestSubscribeMessage({
tmplIds: [TemplateId],
complete (res) {
console.log("complete: ",res)
wx.setStorageSync('TemplateId', res[TemplateId])
},
})
}
效果展示:
PC效果:
真机效果:
3)发送订阅消息
后端服务首先调用getAccessToken
接口,获取接口调用凭证。再调用sendMessage
接口,实现向用户推送一条已订阅的模板消息。
① 获取接口调用凭证
token
有效期为7200s
详情请见: 微信官方文档-获取接口调用凭据
接口:
GET https://api.weixin.qq.com/cgi-bin/token
请求参数:
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
grant_type | string | 是 | 填写 client_credential |
appid | string | 是 | 唯一凭证,即 AppID ,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态) |
secret | string | 是 | 唯一凭证密钥,即 AppSecret ,获取方式同 appid |
返回参数:
属性 | 类型 | 说明 |
---|---|---|
access_token | string | 获取到的凭证 |
expires_in | number | 凭证有效时间,单位:秒。目前是7200秒之内的值。 |
其他说明:
access_token
的存储至少要保留 512 个字符空间;access_token
的有效期目前为 2 个小时,需定时刷新,重复获取将导致上次获取的access_token
失效;access_token
的有效期通过返回的expires_in
来传达,目前是7200秒之内的值,中控服务器需要根据这个有效时间提前去刷新。在刷新过程中,中控服务器可对外继续输出的老access_token
,此时公众平台后台会保证在5分钟内,新老access_token
都可用,这保证了第三方业务的平滑过渡;
示例代码:
private String getAccessToken(){
// 这里可以结合redis进行缓存
RestTemplate restTemplate = new RestTemplate();
String accessTokenJson = restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/token" +
"?grant_type=client_credential" +
"&appid=" + appletAppid +
"&secret=" + appletSecret, String.class);
Map bean = JSONUtil.parseObj(accessTokenJson).toBean(Map.class);
if (bean.get("access_token") != null){
return bean.get("access_token").toString();
}else {
throw new RuntimeException("凭证获取失败, 错误码:" + bean.get("errcode") + " 错误信息:" + bean.get("errmsg"));
}
}
② 推送订阅消息
详情请见: 微信官方文档-推送订阅消息
接口:
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN
Post参数:
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
access_token | string | 是 | 接口调用凭证,该参数为 URL 参数,非 Body 参数。使用access_token或者authorizer_access_token |
template_id | string | 是 | 所需下发的订阅模板id |
page | string | 否 | 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar )。该字段不填则模板无跳转 |
touser | string | 是 | 接收者(用户)的 openid |
data | string | 是 | 模板内容,格式形如 { “key1 ”: { “value”: any }, “key2 ”: { “value”: any } }的object |
miniprogram_state | string | 是 | 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 |
lang | string | 是 | 进入小程序查看”的语言类型,支持zh_CN (简体中文)、en_US (英文)、zh_HK (繁体中文)、zh_TW (繁体中文),默认为zh_CN |
返回参数:
属性 | 类型 | 说明 |
---|---|---|
errcode | number | 错误码 |
errmsg | string | 错误信息 |
错误码:
错误码 | 错误码取值 | 解决方案 |
---|---|---|
40001 | invalid credential access_token isinvalid or not latest | 获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口 |
40003 | invalid openid | 不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID |
40014 | invalid access_token | 不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口 |
40037 | invalid template_id | 不合法的 template_id |
43101 | 用户未订阅消息 | 检查订阅弹窗回调结果或事件推送确认是否订阅成功,检查是否一次性订阅的次数之前已下发完 |
43107 | 订阅消息能力封禁 | 检查账号是否被封禁订阅消息能力,检查模板id对应的模板是否被封禁 |
43108 | 并发下发消息给同一个粉丝 | 检查是否有同时下发多个消息给同一粉丝的情况 |
45168 | 命中敏感词 | 检查下发消息中是否带有敏感词 |
47003 | 参数错误 | 根据wiki 文档检查data结构格式是否正确,检查各个关键词是否满足对应规则 |
其他说明:
参数类别 | 参数说明 | 参数值限制 | 说明 |
---|---|---|---|
thing.DATA | 事物 | 20个以内字符 | 可汉字、数字、字母或符号组合 |
number.DATA | 数字 | 32位以内数字 | 只能数字,可带小数 |
letter.DATA | 字母 | 32位以内字母 | 只能字母 |
symbol.DATA | 符号 | 5位以内符号 | 只能符号 |
character_string.DATA | 字符串 | 32位以内数字、字母或符号 | 可数字、字母或符号组合 |
time.DATA | 时间 | 24小时制时间格式(支持+年月日),支持填时间段,两个时间点之间用“~”符号连接 | 例如:15:01 ,或:2019年10月1日 15:01 |
date.DATA | 日期 | 年月日格式(支持+24小时制时间),支持填时间段,两个时间点之间用“~”符号连接 | 例如:2019年10月1日 ,或:2019年10月1日 15:01 |
amount.DATA | 金额 | 1个币种符号+10位以内纯数字,可带小数,结尾可带“元” | 可带小数 |
phone_number.DATA | 电话 | 17位以内,数字、符号 | 电话号码,例:+86-0766-66888866 |
car_number.DATA | 车牌 | 8位以内,第一位与最后一位可为汉字,其余为字母或数字 | 车牌号码:粤A8Z888挂 |
name.DATA | 姓名 | 10个以内纯汉字或20个以内纯字母或符号 | 中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内 |
phrase.DATA | 汉字 | 5个以内汉字 | 5个以内纯汉字,例如:配送中 |
enum.DATA | 枚举值 | 只能上传枚举值范围内的字段值 | 调用接口获取参考枚举值 |
符号表示除中文、英文、数字外的常见符号,不能带有换行等控制字符。 时间格式支持HH:MM:SS
或者HH:MM
。 日期包含年月日,为y年m月d日,y年m月、m月d日格式,或者用‘-’、‘/’、‘.’符号连接,如2018-01-01,2018/01/01,2018.01.01,2018-01,01-01。 每个模板参数都会以类型为前缀,例如第一个数字模板参数为number01.DATA
,第二个为number02.DATA
示例代码:
public void appletSendMessage() {
String accessToken = getAccessToken();
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> map = new HashMap<String, Object>();
map.put("touser", "用户的openid");
map.put("template_id", "消息模板id");
// 模板数据
Map<String, Object> data = new HashMap<String, Object>();
data.put("thing1", Collections.singletonMap("value", "测试订阅推送"));
data.put("number2", Collections.singletonMap("value", 10001));
data.put("amount3", Collections.singletonMap("value", "100元"));
data.put("thing4", Collections.singletonMap("value", "测试订单消息推送"));
map.put("data", data);
HttpEntity<Map<String, Object>> request = new HttpEntity<Map<String, Object>>(map);
String resultJson = restTemplate.postForObject("https://api.weixin.qq.com/cgi-bin/message/subscribe/send" +
"?access_token=" + accessToken, request, String.class);
Map bean = JSONUtil.parseObj(resultJson).toBean(Map.class);
if (Integer.parseInt(bean.get("errcode").toString()) != 0){
throw new RuntimeException("发送失败, 错误码:" + bean.get("errcode") + " 错误信息:" + bean.get("errmsg"));
}
}
效果展示:
1.4.2 长期订阅
目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下。 因为目前没有涉及线下服务业务,所以本文不进行过多介绍。
2. 公众号消息推送
2.1 一次性订阅消息
2.1.1 功能介绍
开发者可以通过一次性订阅消息授权让微信用户授权第三方移动应用(接入说明)或公众号,获得发送一次订阅消息给到授权微信用户的机会。授权微信用户可以不需要关注公众号。微信用户每授权一次,开发者可获得一次下发消息的权限。(注意:同一用户在同一scene场景值下的多次授权不累积下发权限,只能下发一条。若要订阅多条,需要不同scene场景值)
消息下发位置说明:对于已关注公众号的,消息将下发到公众号会话里;未关注公众号的,将下发到服务通知。
详情请见:微信官方文档-公众号一次性订阅消息
2.1.2 前提条件
一个已认证的服务号,一个已备案域名。
2.1.3 准备工作
**注意:**若服务号未进行认证,则不能进行相关信息的配置。
配置业务域名
登录服务号账号后,点击设置与开发下的公众号设置,选择功能设置,找到业务域名进行设置。
这里我把TXT
文件放到了后端服务的 resources下的static目录下,通过nps
的使用https
,即可将服务请求转发到本机后端服务完成校验。
2.1.4 消息推送
1)用户授权
在确保微信公众账号拥有订阅消息授权的权限的前提下(已认证的公众号即有权限,可登录公众平台在接口权限列表处查看),引导用户在微信客户端打开如下链接:
https://mp.weixin.qq.com/mp/subscribemsg?action=get_confirm&appid=wxaba38c7f163da69b&scene=1000&template_id=1uDxHNXwYQfBmXOfPJcjAS3FynHArD8aWMEFNRGSbCc&redirect_url=http%3a%2f%2fsupport.qq.com&reserved=test#wechat_redirect
**注意:**该参数中的模板id在接口权限列表中查看,并非准备阶段中的模板id。若使用准备阶段模板id,则会提示模板id不对。
参数:
参数 | 是否必须 | 说明 |
---|---|---|
action | 是 | 直接填get_confirm即可 |
appid | 是 | 公众号的唯一标识 |
scene | 是 | 重定向后会带上scene参数,开发者可以填0-10000的整型值,用来标识订阅场景值 |
template_id | 是 | 订阅消息模板ID,登录公众平台后台,在接口权限列表处可查看订阅模板ID |
redirect_url | 是 | 授权后重定向的回调地址,请使用UrlEncode 对链接进行处理。 注:要求redirect_url 的域名要跟登记的业务域名一致,且业务域名不能带路径。 业务域名需登录公众号,在设置-公众号设置-功能设置里面对业务域名设置。 |
reserved | 否 | 用于保持请求和回调的状态,授权请后原样带回给第三方。该参数可用于防止csrf 攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验,开发者可以填写a-zA-Z0-9 的参数值,最多128字节,要求做urlencode |
#wechat_redirect | 是 | 无论直接打开还是做页面302重定向时,必须带此参数 |
示例代码:
// 这里通过生成二维码引导用户授权
public void officialAuth(HttpServletResponse response) {
String url = "https://mp.weixin.qq.com/mp/subscribemsg" +
"?action=get_confirm" +
"&appid=" + officialAppid +
"&scene=" + "0001" +
"&template_id=" + "接口权限列表处一次性订阅消息模板id" +
"&redirect_url=" + "https://业务域名/wx/official/send( urlEncode 进行处理)" +
"#wechat_redirect";
response.setContentType("image/png");
try {
QrCodeUtil.generate(url,300,300,"jpg",response.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
打开页面localhost:8080/wx/official/auth
,通过微信扫一扫进行扫码,点击确认接收。
效果展示:
回调接口:
如果用户点击同意或取消授权,页面将跳转至:
redirect_url/?openid=OPENID&template_id=TEMPLATE_ID&action=ACTION&scene=SCENE
参数:
参数 | 说明 |
---|---|
openid | 用户唯一标识,只在用户确认授权时才会带上 |
template_id | 订阅消息模板ID |
action | 用户点击动作,"confirm"代表用户确认授权,"cancel"代表用户取消授权 |
scene | 订阅场景值 |
reserved | 请求带入原样返回 |
示例代码:
public void getOfficialOpenid(Map<String, Object> params) {
String openid = params.get("openid").toString();
if (openid != null){
saveOpenid(openid);
}else {
String action = "confirm".equals(params.get("action").toString()) ? "确定授权" : "取消授权";
throw new RuntimeException("openid获取失败, 用户行为:" + action);
}
}
2)发送订阅消息
① 获取接口调用凭证
获取方式如同 1.4.1节第三部分第一步。
② 推送订阅消息
通过API
推送订阅模板消息给到授权微信用户。
接口:
POST https://api.weixin.qq.com/cgi-bin/message/template/subscribe?access_token=ACCESS_TOKEN
Post参数:
参数 | 是否必须 | 说明 |
---|---|---|
touser | 是 | 填接收消息的用户openid |
template_id | 是 | 接口权限列表处订阅消息模板ID |
url | 否 | 点击消息跳转的链接,需要有ICP 备案 |
miniprogram | 否 | 跳小程序所需数据,不需跳小程序可不用传该数据 |
appid | 是 | 所需跳转到的小程序appid (该小程序appid 必须与发模板消息的公众号是绑定关联关系,并且小程序要求是已发布的) |
pagepath | 是 | 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar ) |
scene | 是 | 订阅场景值 |
title | 是 | 消息标题,15字以内 |
data | 是 | 消息正文,value为消息内容文本(200字以内),没有固定格式,可用\n换行,color为整段消息内容的字体颜色(目前仅支持整段消息为一种颜色) |
注:url
和miniprogram
都是非必填字段,若都不传则模板无跳转;若都传,会优先跳转至小程序。开发者可根据实际需要选择其中一种跳转方式即可。当用户的微信客户端版本不支持跳小程序时,将会跳转至url
。****
返回说明:
{
"errcode":0,
"errmsg":"ok"
}
示例代码:
public void officialSendMessage() {
String accessToken = getAccessToken(officialAppid, officialSecret);
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> map = new HashMap<String, Object>();
map.put("touser", "用户openid");
map.put("template_id", "接口权限列表处一次性订阅消息模板id");
map.put("title", "阅读消息推送");
map.put("scene", "0001");
map.put("data", Collections.singletonMap("content", Collections.singletonMap("value", "文章制作不易,请留下一个小赞,谢谢。")));
String resultJson = restTemplate.postForObject("https://api.weixin.qq.com/cgi-bin/message/template/subscribe?access_token=" + accessToken, map, String.class);
Map result = JSONUtil.parseObj(resultJson).toBean(Map.class);
if (Integer.parseInt(result.get("errcode").toString()) != 0){
throw new RuntimeException("发送失败, 错误码:" + result.get("errcode") + " 错误信息:" + result.get("errmsg"));
}
}
效果展示:
2.2 模板消息
2.2.1 功能介绍
模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。
详情请见:微信官方文档-模板消息
2.2.2 前提条件
一个已注册服务号,已备案域名。
若没有注册服务号,也可以使用测试账户。
若没有备案域名,也可以使用免费Cpolar内网穿透工具。
本文使用已注册服务号进行演示。
2.2.3 准备工作
1)设置服务类目
登录服务号账号后,点击设置与开发下的公众号设置,选择账号详情,找到服务类目,点击详情,进行设置。
2) 开通模板消息
登录服务号账号后,点击**+ 新的功能**,找到 广告与服务中的模板消息 ,点击去开通,进行开通。
3)选择模板消息
登录服务号账号后,点击广告与服务下的模板消息,选择模板库,选择模板。若没有贴合业务的模板,可点击右上角 帮助我们完善类目模板库,进行模板申请。
4)网页授权及业务域名配置
登录服务号账号后,点击设置与开发下的公众号设置,选择功能设置,找到网页授权域名和业务域名,点击设置,进行配置。
2.2.4 消息推送
1)获取并保存用户openid
详情请见: 微信官方文档-网页授权
引导用户在微信客户端打开网页授权页面,获取登录凭证。
接口:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。
**尤其注意:**由于授权操作安全等级较高,所以在发起授权请求时,微信会对授权链接做正则强匹配校验,如果链接的参数顺序不对,授权页面将无法正常访问。
参数:
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 公众号的唯一标识 |
redirect_uri | 是 | 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理 |
response_type | 是 | 返回类型,请填写code |
scope | 是 | 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid ),snsapi_userinfo (弹出授权页面,可通过openid 拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 ) |
state | 否 | 重定向后会带上state参数,开发者可以填写a-zA-Z0-9 的参数值,最多128字节 |
#wechat_redirect | 是 | 无论直接打开还是做页面302重定向时候,必须带此参数 |
forcePopup | 否 | 强制此次授权需要用户弹窗确认;默认为false;需要注意的是,若用户命中了特殊场景下的静默授权逻辑,则此参数不生效 |
下图为scope等于snsapi_userinfo
时的授权页面:
示例代码:
// 这里通过生成二维码引导用户授权
public void officialRouter(HttpServletResponse response) {
String redirectUrl = URLEncoder.encode("https://" + doMainName + "/wx/official/login");
String url = "https://open.weixin.qq.com/connect/oauth2/authorize" +
"?appid=" + officialAppid +
"&redirect_uri=" + redirectUrl +
"&response_type=code" +
"&scope=snsapi_base" +
"&state=" + "0001" +
"#wechat_redirect";
response.setContentType("image/png");
try {
QrCodeUtil.generate(url,300,300,"jpg",response.getOutputStream());
}catch (IOException e){
throw new RuntimeException(e);
}
}
用户同意授权后
如果用户同意授权,页面将跳转至
redirect_uri/?code=CODE&state=STATE。
code说明:
code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
错误码:
返回码 | 说明 |
---|---|
10003 | redirect_uri 域名与后台配置不一致 |
10004 | 此公众号被封禁 |
10005 | 此公众号并没有这些scope的权限 |
10006 | 必须关注此测试号 |
10009 | 操作太频繁了,请稍后重试 |
10010 | scope不能为空 |
10011 | redirect_uri 不能为空 |
10012 | appid 不能为空 |
10013 | state不能为空 |
10015 | 公众号未授权第三方平台,请检查授权状态 |
10016 | 不支持微信开放平台的Appid ,请使用公众号Appid |
示例代码:
public void officialLogin(Map<String, Object> params) {
RestTemplate restTemplate = new RestTemplate();
String resultJson = restTemplate.getForObject("https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=" + officialAppid +
"&secret=" + officialSecret +
"&code=" + params.get("code").toString() +
"&grant_type=authorization_code", String.class).toString();
Map result = JSONUtil.parseObj(resultJson).toBean(Map.class);
if (result.get("openid") != null){
saveOpenid(result.get("openid").toString());
}else {
throw new RuntimeException("openid获取失败, 错误码:" + result.get("errorcode") + "错误信息:" + result.get("errmsg"));
}
}
2)消息推送
接口:
POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
Post参数:
参数 | 是否必填 | 说明 |
---|---|---|
touser | 是 | 接收者openid |
template_id | 是 | 模板ID |
url | 否 | 模板跳转链接(海外账号没有跳转能力) |
miniprogram | 否 | 跳小程序所需数据,不需跳小程序可不用传该数据 |
appid | 是 | 所需跳转到的小程序appid (该小程序appid 必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏) |
pagepath | 否 | 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar ),要求该小程序已发布,暂不支持小游戏 |
data | 是 | 模板数据 |
client_msg_id | 否 | 防重入id。对于同一个openid + client_msg_id , 只发送一条消息,10分钟有效,超过10分钟不保证效果。若无防重入需求,可不填 |
注:url
和miniprogram
都是非必填字段,若都不传则模板无跳转;若都传,会优先跳转至小程序。开发者可根据实际需要选择其中一种跳转方式即可。当用户的微信客户端版本不支持跳小程序时,将会跳转至url
。
返回码说明
返回码 | 含义 |
---|---|
43116 | 该模板因滥用被滥用过多,已被限制下发 |
40249 | 不支持下发营销/推广类的消息内容 |
40250 | 下发消息内容不规范(包含空值等),建议检查内容规范性后再下发 |
40251 | 因历史违规导致平台限制账号调用上限,当前已到达下发上限 |
40252 | 正在调用的模板下发的部分内容已进入平台审核流程,在审核完成前,相关内容暂时无法下发 |
其他说明:
参数类别 | 参数说明 | 参数值限制 | 说明 | 适用范围 |
---|---|---|---|---|
thing.DATA | 事物 | 20个以内字符 | 可汉字、数字、字母或符号组合 | 供姓名关键词、地址关键词、机构/组织名关键词选择(如:单位名、银行名、医院名、科室、班级)、品名关键词选择(如:药品名、股票名、课程名、科目名、岗位名) |
character_string.DATA | 字符串 | 32位以内数字、字母或符号 | 可数字、字母或符号组合 | 供数字/编码/编号/单号/卡号/航班号关键词选择(如:证券编码、设备编号、快递单号、银行卡号、网址) |
time.DATA | 时间 | 24小时制时间格式(支持+年月日),支持填时间段,两个时间点之间用“~”符号连接 | 例如:15:01 ,或:2019年10月1日 15:01 | 供时间类关键词选择 |
amount.DATA | 金额 | 1个币种符号+12位以内纯数字,可带小数,结尾可带“元” | 可带小数 | 供金额类关键词选择 |
phone_number.DATA | 电话 | 17位以内,数字、符号 | 电话号码,例:+86-0766-66888866 | 供电话号码类关键词选择 |
car_number.DATA | 车牌 | 8位以内,第一位与最后一位可为汉字,其余为字母或数字 | 车牌号码:粤A8Z888挂 | 供车牌号码类关键词选择 |
const.DATA | 常量 | 20位以内字符,超过无法下发注:需枚举(需将内容提交平台审核,审核通过可下发) | 只能下发审核通过的字符串和空串 | 供状态/方式/类型/提醒/说明/详情关键词选择(如:支付状态、排队状态、天气状态、物流状态、用药提醒、还款提醒) |
符号表示除中文、英文、数字外的常见符号,不能带有换行等控制字符。 时间格式支持HH:MM:SS
或者HH:MM
。 日期包含年月日,为y年m月d日,y年m月、m月d日格式,或者用‘-’、‘/’、‘.’符号连接,如2018-01-01,2018/01/01,2018.01.01,2018-01,01-01。 每个模板参数都会以类型为前缀,例如第一个数字模板参数为number01.DATA
,第二个为number02.DATA
代码示例:
public void officialTemplateSend() {
String accessToken = getAccessToken(officialAppid, officialSecret);
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> map = new HashMap<String, Object>();
map.put("touser", "用户openid");
map.put("template_id", "公众号下模板消息的模板id");
Map<String, Object> data = new HashMap<String, Object>();
data.put("thing3", Collections.singletonMap("value", "测试模板"));
data.put("thing13", Collections.singletonMap("value", "张三"));
data.put("thing6", Collections.singletonMap("value", "李四"));
data.put("time5", Collections.singletonMap("value", "2024-07-30"));
map.put("data", data);
String resultJson = restTemplate.postForObject("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + accessToken, map, String.class);
Map result = JSONUtil.toBean(resultJson, Map.class);
if (0 != Integer.parseInt(result.get("errcode").toString())){
throw new RuntimeException("发送失败, 错误码:" + result.get("errcode") + " 错误信息:" + result.get("errmsg"));
}
}
效果展示:
3. 后端代码仓库
代码地址:微信消息推送后端演示代码