最近开发微信和小程序,需要后台实现推送,所以就动手实现一下小程序模版消息功能的推送。
我们先来看看官方的说明:
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
请求地址
POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
请求参数
参数 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
access_token | string | 是 | 接口调用凭证 | |
touser | string | 是 | 用户openid,可以是小程序的openid,也可以是mp_template_msg.appid对应的公众号的openid | |
weapp_template_msg | Object | 否 | 小程序模板消息相关的信息,可以参考小程序模板消息接口; 有此节点则优先发送小程序模板消息 | |
mp_template_msg | Object | 是 | 公众号模板消息相关的信息,可以参考公众号模板消息接口;有此节点并且没有weapp_template_msg节点时,发送公众号模板消息 |
为了使第三方开发者能够为用户提供更多更有价值的个性化服务,微信公众平台 开放了许多接口,包括自定义菜单接口、客服接口、获取用户信息接口、用户分组接口、群发接口等,access_token接口调用凭证是一个重要的参数,但是这个代表的是什么意思呢?
access_token简介
开发者在调用这些接口时,都需要传入一个相同的参数 access_token,它是公众账号的全局唯一票据,它是接口访问凭证。
access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。
access_token 的存储至少要保留 512 个字符空间;
access_token 的有效期目前为 2 个小时,需定时刷新,重复获取将导致上次获取的 access_token 失效;
建议开发者使用中控服务器统一获取和刷新 access_token,其他业务逻辑服务器所使用的 access_token 均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致 access_token 覆盖而影响业务;
access_token 的有效期通过返回的 expire_in 来传达,目前是7200秒之内的值,中控服务器需要根据这个有效时间提前去刷新。在刷新过程中,中控服务器可对外继续输出的老 access_token,此时公众平台后台会保证在5分钟内,新老 access_token 都可用,这保证了第三方业务的平滑过渡;
access_token 的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新 access_token 的接口,这样便于业务服务器在API调用获知 access_token 已超时的情况下,可以触发 access_token 的刷新流程。
公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在微信公众平台官网-开发者中心页中获得(需要已经成为开发者,且帐号没有异常状态)。
问题:如何通过获取access_token?
解决方案:(1)直接通过浏览器访问。(2)编写程序,模拟https连接,获得access_token。
解决详细步骤如下:
(1)浏览器中直接输入链接:https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&type=%E5%9F%BA%E7%A1%80%E6%94%AF%E6%8C%81&form=%E8%8E%B7%E5%8F%96access_token%E6%8E%A5%E5%8F%A3%20/token&token=&lang=zh_CN
然后把APPID和APPSECRET替换成自己的appid和appsecret,在浏览器即可获得access_token。
(2)如何在程序中模拟发送https请求,并且获取到access_token呢
我们再看看官方的说明:
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183
请求地址
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
请求参数
参数 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
grant_type | string | 是 | 填写client_credential | |
appid | string | 是 | 小程序唯一凭证,即 AppID | |
secret | string | 是 | 小程序唯一凭证密钥,即 AppSecret,获取方式同 appid |
返回值
Object
返回的 JSON 数据包
属性 | 类型 | 说明 |
---|---|---|
access_token | string | 获取到的凭证 |
expires_in | number | 凭证有效时间,单位:秒。目前是7200秒之内的值 |
errcode | number | 错误码 |
errmsg | string | 错误信息 |
errcode 的合法值
值 | 说明 |
---|---|
-1 | 系统繁忙,此时请开发者稍候再试 |
0 | 请求成功 |
40001 | AppSecret错误或者AppSecret不属于这个小程序,请开发者确认AppSecret的正确性 |
40002 | 请确保grant_type字段值为client_credential |
40013 | 不合法的 AppID,请开发者检查 AppID 的正确性,避免异常字符,注意大小写 |
返回数据示例
正常情况下,微信会返回下述JSON数据包
{"access_token":"ACCESS_TOKEN","expires_in":7200}
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppSecret无效错误)
{"errcode":40001,"errmsg":"invalid AppSecret"}
完整代码如下:
package com.study.dto;
/**
* @Auther: lds
* @Date: 2019/7/25 16:03
* @Description: 微信小程序发送模板消息(服务通知)的入参
*/
public class WxMessageDTO {
private String touser; // 用户的openid
private String template_id; // 所需下发的模板消息的id
private String form_id; // 表单提交场景下,为submit事件带上的form_id;支付场景下,为本次支付的prepay_id
private String activityname; // 活动名称
private String lotterystate; // 用户是否中奖
private String lotterytime; // 抽奖日期
@Override
public String toString() {
return "WxMessageSendDTO{" +
", touser='" + touser + '\'' +
", template_id='" + template_id + '\'' +
", form_id='" + form_id + '\'' +
", activityname='" + activityname + '\'' +
", lotterystate='" + lotterystate + '\'' +
", lotterytime='" + lotterytime + '\'' +
'}';
}
public String getTouser() {
return touser;
}
public void setTouser(String touser) {
this.touser = touser;
}
public String getTemplate_id() {
return template_id;
}
public void setTemplate_id(String template_id) {
this.template_id = template_id;
}
public String getForm_id() {
return form_id;
}
public void setForm_id(String form_id) {
this.form_id = form_id;
}
public String getActivityname() {
return activityname;
}
public void setActivityname(String activityname) {
this.activityname = activityname;
}
public String getLotterystate() {
return lotterystate;
}
public void setLotterystate(String lotterystate) {
this.lotterystate = lotterystate;
}
public String getLotterytime() {
return lotterytime;
}
public void setLotterytime(String lotterytime) {
this.lotterytime = lotterytime;
}
}
package com.study.dto;
import com.alibaba.fastjson.JSONObject;
/**
* @Auther: lds
* @Date: 2019/7/25 17:20
* @Description: 拼接模板消息相关的字段
*/
public class WeappTemplateMsgDTO {
private String template_id; // 所需下发的模板消息的id
private String page; // 点击模板卡片后跳转页面,仅限小程序内的页面
private String form_id; // 表单提交场景下,为submit事件带上的form_id;支付场景下,为本次支付的prepay_id
private JSONObject data; // 小程序模板消息发送的数据,不填则发送空模板
@Override
public String toString() {
return "WeappTemplateMsg{" +
"template_id='" + template_id + '\'' +
", page='" + page + '\'' +
", form_id='" + form_id + '\'' +
", data=" + data +
'}';
}
public String getTemplate_id() {
return template_id;
}
public void setTemplate_id(String template_id) {
this.template_id = template_id;
}
public String getPage() {
return page;
}
public void setPage(String page) {
this.page = page;
}
public String getForm_id() {
return form_id;
}
public void setForm_id(String form_id) {
this.form_id = form_id;
}
public JSONObject getData() {
return data;
}
public void setData(JSONObject data) {
this.data = data;
}
}
package com.study.dto;
import com.alibaba.fastjson.JSONObject;
/**
* @Auther: lds
* @Date: 2019/7/25 16:03
* @Description: 发送消息需要的入参
*/
public class WxMessageSendDTO {
private String touser; // 用户的openid
private WeappTemplateMsgDTO weapp_template_msg; // 小程序模板消息相关的信息
@Override
public String toString() {
return "WxMessageSendDTO{" +
"touser='" + touser + '\'' +
", weapp_template_msg=" + weapp_template_msg +
'}';
}
public String getTouser() {
return touser;
}
public void setTouser(String touser) {
this.touser = touser;
}
public WeappTemplateMsgDTO getWeapp_template_msg() {
return weapp_template_msg;
}
public void setWeapp_template_msg(WeappTemplateMsgDTO weapp_template_msg) {
this.weapp_template_msg = weapp_template_msg;
}
}
package com.study.controller;
import com.alibaba.fastjson.JSONObject;
import com.study.dto.WxMessageDTO;
import com.study.service.WxMessageSendService;
import com.study.utils.ReturnInfo;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @Auther: lds
* @Date: 2019/7/25 16:03
* @Description: 微信小程序发送服务通知
*/
@RestController
@RequestMapping(value = "/wxMessage")
public class WxMessageSendController {
private static final Logger logger = LoggerFactory.getLogger(WxMessageSendController.class);
@Autowired
private WxMessageSendService wxMessageSendService;
/**
* 功能描述:微信小程序发送模板消息(服务通知)
*
* 入参:WxMessageSendDTO
* 逻辑:根据微信官方API,调用微信官方接口先获取access_token接口调用凭证,
* 在根据消息模板拼接的需要发送的内容,调用微信官方接口发送消息给用户
* code:
* 入参有空,code:2001
* 系统异常,code:2002
*/
@RequestMapping(value = "/send", method = RequestMethod.POST)
public String sendWxMessage(@RequestBody WxMessageDTO wxMessageDTO) {
logger.info("WxMessageSendController Method sendWxMessage 传递参数 WxMessageDTO: " + JSONObject.toJSONString(wxMessageDTO));
ReturnInfo tReturnInfo = new ReturnInfo();
// 判断入参
if (wxMessageDTO == null) {
tReturnInfo.setCode("2001");
tReturnInfo.setFlag("false");
tReturnInfo.setMes("param is null");
tReturnInfo.setData(null);
return JSONObject.toJSONString(tReturnInfo);
}
ReturnInfo temp = checkNull(wxMessageDTO);
if ("false".equals(temp.getFlag())) {
tReturnInfo.setCode("2001");
tReturnInfo.setFlag("false");
tReturnInfo.setMes(temp.getMes());
tReturnInfo.setData(null);
return JSONObject.toJSONString(tReturnInfo);
}
// 拼接微信推送的模板
String errcode = wxMessageSendService.wxMessageSend(wxMessageDTO);
if ("0".equals(errcode)) {
tReturnInfo.setCode("0");
tReturnInfo.setFlag("true");
tReturnInfo.setMes("微信小程序发送微信消息成功");
} else if ("40037".equals(errcode)) {
tReturnInfo.setCode("40037");
tReturnInfo.setFlag("false");
tReturnInfo.setMes("模板ID不正确");
} else if ("41028".equals(errcode)) {
tReturnInfo.setCode("41028");
tReturnInfo.setFlag("false");
tReturnInfo.setMes("form_id过期或者不正确");
} else if ("41029".equals(errcode)) {
tReturnInfo.setCode("41029");
tReturnInfo.setFlag("false");
tReturnInfo.setMes("form_id已被使用");
} else if ("41030".equals(errcode)) {
tReturnInfo.setCode("41030");
tReturnInfo.setFlag("false");
tReturnInfo.setMes("page不正确");
} else if ("45009".equals(errcode)) {
tReturnInfo.setCode("45009");
tReturnInfo.setFlag("false");
tReturnInfo.setMes("接口调用超过限额");
} else if ("40003".equals(errcode)) {
tReturnInfo.setCode("40003");
tReturnInfo.setFlag("false");
tReturnInfo.setMes("openid不正确");
} else if ("40013".equals(errcode)) {
tReturnInfo.setCode("40013");
tReturnInfo.setMes("不合法的APPID");
}
return JSONObject.toJSONString(tReturnInfo);
}
private ReturnInfo checkNull(WxMessageDTO wxMessageDTO) {
ReturnInfo tReturnInfo = new ReturnInfo();
String mes = "";
if (StringUtils.isBlank(wxMessageDTO.getTouser())) {
mes += "touser不能为空 ";
}
if (StringUtils.isBlank(wxMessageDTO.getForm_id())) {
mes += "form_id不能为空 ";
}
if (!"".equals(mes)) {
tReturnInfo.setFlag("false");
tReturnInfo.setMes(mes);
} else {
tReturnInfo.setFlag("true");
}
return tReturnInfo;
}
}
package com.study.service;
import com.study.dto.WxMessageDTO;
/**
* @Auther: lds
* @Date: 2019/7/25 16:03
* @Description:
*/
public interface WxMessageSendService {
public String wxMessageSend(WxMessageDTO wxMessageDTO);
}
package com.study.service.impl;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.study.dto.WeappTemplateMsgDTO;
import com.study.dto.WxMessageDTO;
import com.study.dto.WxMessageSendDTO;
import com.study.service.WxMessageSendService;
import com.study.utils.ReturnInfo;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.TimeUnit;
/**
* @Auther: lds
* @Date: 2019/7/25 16:03
* @Description:
*/
@Service
public class WxMessageSendServiceImpl implements WxMessageSendService {
private static final Logger logger = LoggerFactory.getLogger(WxMessageSendServiceImpl.class);
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RestTemplate restTemplate;
@Value("${wxmini.appid}")
private String appid; // 小程序唯一凭证
@Value("${wxmini.appsecret}")
private String appsecret; // 小程序唯一凭证密钥
@Value("${wxmini.templateid}")
private String templateid; // 模板消息ID
@Value("${wxmini.page}")
private String page; // 跳转的页面
// redis中保存的access_token
public static final String WXNIMI_ACCESS_TOKEN = "WXNIMI_ACCESS_TOKEN";
// access_token接口调用凭证
public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" +
"&appid=APPID&secret=APPSECRET";
// 微信消息发送
public static final String REQUEST_URL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?" +
"access_token=ACCESS_TOKEN";
@Override
public String wxMessageSend(WxMessageDTO wxMessageDTO) {
logger.info("WxMessageSendServiceImpl Method wxMessageSend 传递参数 wxMessageDTO: " + JSONObject.toJSONString(wxMessageDTO));
JSONObject postResultJson = null;
String access_token = null;
String errcode = null;
try {
String touser = wxMessageDTO.getTouser();
// 获取access_token接口调用凭证
access_token = this.redisTemplate.opsForValue().get(WXNIMI_ACCESS_TOKEN);
if (StringUtils.isBlank(access_token)) {
ReturnInfo tReturnInfo = getAccess_token(appid, appsecret);
access_token = tReturnInfo.getData();
this.redisTemplate.opsForValue().set(WXNIMI_ACCESS_TOKEN, access_token, 2, TimeUnit.HOURS);
}
logger.info("#### 获取的access_token: " + access_token);
// 将URL中的ACCESS_TOKEN替换掉
String requestUrl = REQUEST_URL.replace("ACCESS_TOKEN", access_token);
// 拼接微信推送的模板
WxMessageSendDTO wxMessage = new WxMessageSendDTO();
wxMessage.setTouser(touser);
WeappTemplateMsgDTO weappTemplateMsgDTO = new WeappTemplateMsgDTO();
weappTemplateMsgDTO.setTemplate_id(wxMessageDTO.getTemplate_id());
weappTemplateMsgDTO.setForm_id(wxMessageDTO.getForm_id());
weappTemplateMsgDTO.setPage(page);
JSONObject keyword1 = new JSONObject();
keyword1.put("value", wxMessageDTO.getActivityname());
JSONObject keyword2 = new JSONObject();
keyword2.put("value", "已开奖,点击查看中奖名单");
JSONObject keyword3 = new JSONObject();
keyword3.put("value", wxMessageDTO.getLotterytime());
JSONObject keyword4 = new JSONObject();
String lotterystate = wxMessageDTO.getLotterystate();
keyword4.put("value", "恭喜!本次活动您获得" + lotterystate);
if ("N".equals(lotterystate)) {
keyword4.put("value", "很遗憾,本次活动您未中奖");
}
JSONObject data = new JSONObject();
data.put("keyword1", keyword1);
data.put("keyword2", keyword2);
data.put("keyword3", keyword3);
data.put("keyword4", keyword4);
weappTemplateMsgDTO.setData(data);
wxMessage.setWeapp_template_msg(weappTemplateMsgDTO);
// 调用微信官方接发送模板消息
String postForObject = this.restTemplate.postForObject(requestUrl, JSONObject.toJSONString(wxMessage), String.class);
postResultJson = JSONObject.parseObject(postForObject);
errcode = postResultJson.getString("errcode");
} catch (Exception e) {
logger.info("#### 微信小程序调用官方微信发模板消息异常", e);
}
return errcode;
}
/**
* @param appid
* @param appsecret
* @return 使用appid、appsecret获取access_token
*/
private ReturnInfo getAccess_token(String appid, String appsecret) {
ReturnInfo tReturnInfo = new ReturnInfo();
try {
// 将URL中的两个参数替换掉
String requestUrl = ACCESS_TOKEN_URL.replace("APPID", appid).replace("APPSECRET", appsecret);
// 调用微信官方接口获取access_token
JSONObject jsonObject = this.restTemplate.getForObject(requestUrl, JSONObject.class);
// 取出access_token
String access_token = jsonObject.getString("access_token");
if (StringUtils.isBlank(access_token)) {
String errcode = jsonObject.getString("errcode");
if ("-1".equals(errcode)) {
tReturnInfo.setCode("-1");
tReturnInfo.setMes("系统繁忙");
} else if ("40001".equals(errcode)) {
tReturnInfo.setCode("40001");
tReturnInfo.setMes("APPSECRET错误,请确认APPSECRET是否正确");
} else if ("40002".equals(errcode)) {
tReturnInfo.setCode("40002");
tReturnInfo.setMes("请确保grant_type字段值为client_credential");
} else if ("40013".equals(errcode)) {
tReturnInfo.setCode("40013");
tReturnInfo.setMes("不合法的APPID");
}
logger.info("#### 微信小程序获取access_token的errcode: " + errcode);
} else {
tReturnInfo.setCode("2000");
tReturnInfo.setData(access_token);
}
} catch (JSONException e) {
logger.info("#### 微信小程序调用官方微信获取access_token接口异常", e);
return tReturnInfo;
}
return tReturnInfo;
}
}
在实际项目中应该将APPID和APPSECRET写在配置文件中,读取一下,绝对不可以通过前端传入!这个动作是非常危险的,会导致数据的泄露!
考虑到安全问题,我对APPID和APPSECRET进行了遮挡,大家可以根据自己的进行修改。
获取到的access_token效果图:
服务通知显示效果: