最近開發微信和小程序,需要後臺實現推送,所以就動手實現一下小程序模版消息功能的推送。
我們先來看看官方的說明:
請求地址
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。
解決詳細步驟如下:
然後把APPID和APPSECRET替換成自己的appid和appsecret,在瀏覽器即可獲得access_token。
(2)如何在程序中模擬發送https請求,並且獲取到access_token呢
我們再看看官方的說明:
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;
/**
* 功能描述:微信小程序發送模板消息(服務通知)
* <p>
* 入參: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<String, String> 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效果圖:
服務通知顯示效果: