最近開發公衆號項目,前端採用vue開發,後臺使用java開發,由於業務需求,需要實現公衆號向用戶發送重要的服務通知,提醒工作人員進行業務審覈。這時候就需要用到微信平臺的模板消息,爲了保證用戶不受到騷擾,在開發者出現需要主動提醒、通知用戶時,才允許開發者在公衆平臺網站中模板消息庫中選擇模板,選擇後獲得模板ID,再根據模板ID向用戶主動推送提醒、通知消息。常用的服務場景,如信用卡刷卡通知,商品下單成功、購買成功通知等。
獲取template_id(注意:僅微信開放平臺同事可獲取)
通過向微信公衆平臺申請模板,來獲取模板id,模板消息調用時主要需要模板ID和模板中各參數的賦值內容。請注意:
1.模板中參數內容必須以".DATA"結尾,否則視爲保留字;
2.模板保留符號"{{ }}"
下圖是在微信測試公衆號申請模板
請求模板消息接口
1)微信網頁授權
//前端發請請求
this.axios.get('/wx/get_code_num').then((res) => {
window.location.href = res.data;
}).catch((error) => {
console.log(error)
});
/**
* 1.用戶同意授權,獲取code
*/
@RequestMapping(value = "/get_code_num", method = RequestMethod.GET)
public String getCode() throws UnsupportedEncodingException {
return "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + Constants.APPID + "&redirect_uri="
+ URLEncoder.encode("http://192.168.0.152:8085/wx/send_wx_msg", "UTF-8") + "&response_type=code&scope="
+ Constants.GRANTSCOPE + "&state=STATE#wechat_redirect";
}
2)獲取用戶openid
/**
* 2.通過code換取網頁授權access_token及openid
*/
@RequestMapping(value = "/send_wx_msg", method = RequestMethod.GET)
public String sendWxMsg(String code) {
String access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token";
String accessTokenObj = HttpClientUtil.sendGet(access_token_url, "appid=" + Constants.APPID + "&secret="
+ Constants.APPSECRET + "&code=" + code + "&grant_type=authorization_code");
JSONObject jsonToken = JSONObject.fromObject(accessTokenObj);
String openId = null;
if (StringUtils.isNotBlank(String.valueOf(jsonToken))) {
openId = jsonToken.getString("openid");
}
logger.info("獲取openid,微信平臺接口返回{}", openId);
return openId;
}
3)組裝、發送模板消息
import java.util.TreeMap;
public class WechatTemplate {
private String touser;//用戶openid
private String template_id;//模板ID
private String url;//URL置空,則在發送後,點擊模板消息會進入一個空白頁面(ios),或無法點擊(android)
private TreeMap<String, TreeMap<String, String>> data; //data數據
public static TreeMap<String, String> item(String value, String color) {
TreeMap<String, String> params = new TreeMap<String, String>();
params.put("value", value);
params.put("color", color);
return params;
}
public TreeMap<String, TreeMap<String, String>> getData() {
return data;
}
public void setData(TreeMap<String, TreeMap<String, String>> data) {
this.data = data;
}
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 getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return "WechatTemplate{" +
"touser='" + touser + '\'' +
", template_id='" + template_id + '\'' +
", url='" + url + '\'' +
", data=" + data +
'}';
}
}
//微信模板接口
private final String SEND_TEMPLATE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send";
//模板消息詳情跳轉URL
private static String url = "https://www.baidu.com/";
@RequestMapping(value = "/send_wx_msg", method = RequestMethod.GET)
public String sendWxMsg(String code) {
String access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token";
String accessTokenObj = HttpClientUtil.sendGet(access_token_url, "appid=" + Constants.APPID + "&secret="
+ Constants.APPSECRET + "&code=" + code + "&grant_type=authorization_code");
JSONObject jsonToken = JSONObject.fromObject(accessTokenObj);
String openId = null;
if (StringUtils.isNotBlank(String.valueOf(jsonToken))) {
openId = jsonToken.getString("openid");
}
logger.info("獲取openid,微信平臺接口返回{}", openId);
String urlToken = "https://api.weixin.qq.com/cgi-bin/token";
String tokenObj = HttpClientUtil.sendGet(urlToken, "grant_type=client_credential" + "&secret=" + Constants.APPSECRET + "&appid=" + Constants.APPID);
JSONObject retToken = JSONObject.fromObject(tokenObj);
String accessToken = String.valueOf(retToken.get("access_token"));
logger.info("獲取access_token,微信平臺接口返回{}", accessToken);
TreeMap<String, TreeMap<String, String>> params = new TreeMap<String, TreeMap<String, String>>();
//根據具體模板參數組裝
params.put("first", WechatTemplate.item("您的戶外旅行活動訂單已經支付完成,可在我的個人中心中查看", "#000000"));
params.put("keyword1", WechatTemplate.item("發現尼泊爾—人文與自然的旅行聖地", "#000000"));
params.put("keyword2", WechatTemplate.item("5000元", "#000000"));
params.put("keyword3", WechatTemplate.item("2019.09.04", "#000000"));
params.put("keyword4", WechatTemplate.item("5", "#000000"));
params.put("remark", WechatTemplate.item("請屆時攜帶好身份證件準時到達集合地點,若臨時退改將產生相應損失,敬請諒解,謝謝!", "#000000"));
WechatTemplate wechatTemplate = new WechatTemplate();
wechatTemplate.setTemplate_id(Constants.TEMPLATEID);
wechatTemplate.setTouser(openId);
wechatTemplate.setUrl(url);
wechatTemplate.setData(params);
JSONObject json = JSONObject.fromObject(wechatTemplate);//將java對象轉換爲json對象
String sendData = json.toString();//將json對象轉換爲字符串
logger.info("板參數組裝{}", sendData);
TreeMap<String, String> treeMap = new TreeMap<String, String>();
treeMap.put("access_token", accessToken);
String retInfo = HttpUtil.doPost(SEND_TEMPLATE_MESSAGE_URL, treeMap, sendData);
logger.info("消息模板返回{}", retInfo);
return retInfo;
}
請求的數據格式
{
"data": {
"first": {
"color": "#000000",
"value": "您的戶外旅行活動訂單已經支付完成,可在我的個人中心中查看"
},
"keyword1": {
"color": "#000000",
"value": "發現尼泊爾—人文與自然的旅行聖地"
},
"keyword2": {
"color": "#000000",
"value": "5000元"
},
"keyword3": {
"color": "#000000",
"value": "2019.09.04"
},
"keyword4": {
"color": "#000000",
"value": "5"
},
"remark": {
"color": "#000000",
"value": "請屆時攜帶好身份證件準時到達集合地點,若臨時退改將產生相應損失,敬請諒解,謝謝!"
}
},
"template_id": "ZUMTnYtG0O4vZSv4bPTtWTOFZ2zirOjaM50GYywRRnA",
"touser": "olv_asx8nmggCQEmAFNbQstx3xd0",
"url": "https://www.baidu.com/"
}
微信平臺返回的結果:
微信公衆號通知消息
工具類:
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;
public class HttpUtil {
private static Logger logger = LoggerFactory.getLogger(HttpUtil.class);
protected static final String POST_METHOD = "POST";
private static final String GET_METHOD = "GET";
static {
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
logger.debug("ClientTrusted");
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
logger.debug("ServerTrusted");
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}};
HostnameVerifier doNotVerify = (s, sslSession) -> true;
try {
SSLContext sc = SSLContext.getInstance("SSL", "SunJSSE");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(doNotVerify);
} catch (Exception e) {
logger.error("Initialization https impl occur exception : {}", e);
}
}
/**
* 默認的http請求執行方法
*
* @param url url 路徑
* @param method 請求的方法 POST/GET
* @param map 請求參數集合
* @param data 輸入的數據 允許爲空
* @return result
*/
private static String HttpDefaultExecute(String url, String method, Map<String, String> map, String data) {
String result = "";
try {
url = setParmas(url, map, null);
result = defaultConnection(url, method, data);
} catch (Exception e) {
logger.error("出錯參數 {}", map);
}
return result;
}
public static String httpGet(String url, Map<String, String> map) {
return HttpDefaultExecute(url, GET_METHOD, map, null);
}
public static String httpPost(String url, Map<String, String> map, String data) {
return HttpDefaultExecute(url, POST_METHOD, map, data);
}
/**
* 默認的https執行方法,返回
*
* @param url url 路徑
* @param method 請求的方法 POST/GET
* @param map 請求參數集合
* @param data 輸入的數據 允許爲空
* @return result
*/
private static String HttpsDefaultExecute(String url, String method, Map<String, String> map, String data) {
try {
url = setParmas(url, map, null);
logger.info(data);
return defaultConnection(url, method, data);
} catch (Exception e) {
logger.error("出錯參數 {}", map);
}
return "";
}
public static String doGet(String url, Map<String, String> map) {
return HttpsDefaultExecute(url, GET_METHOD, map, null);
}
public static String doPost(String url, Map<String, String> map, String data) {
return HttpsDefaultExecute(url, POST_METHOD, map, data);
}
/**
* @param path 請求路徑
* @param method 方法
* @param data 輸入的數據 允許爲空
* @return
* @throws Exception
*/
private static String defaultConnection(String path, String method, String data) throws Exception {
if (StringUtils.isBlank(path)) {
throw new IOException("url can not be null");
}
String result = null;
URL url = new URL(path);
HttpURLConnection conn = getConnection(url, method);
if (StringUtils.isNotEmpty(data)) {
OutputStream output = conn.getOutputStream();
output.write(data.getBytes(StandardCharsets.UTF_8));
output.flush();
output.close();
}
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream input = conn.getInputStream();
result = IOUtils.toString(input, StandardCharsets.UTF_8);
input.close();
conn.disconnect();
}
// log.info(result);
return result;
}
/**
* 根據url的協議選擇對應的請求方式
*
* @param url 請求路徑
* @param method 方法
* @return conn
* @throws IOException 異常
*/
//待改進
protected static HttpURLConnection getConnection(URL url, String method) throws IOException {
HttpURLConnection conn;
if (StringUtils.equals("https", url.getProtocol())) {
conn = (HttpsURLConnection) url.openConnection();
} else {
conn = (HttpURLConnection) url.openConnection();
}
if (conn == null) {
throw new IOException("connection can not be null");
}
conn.setRequestProperty("Pragma", "no-cache");// 設置不適用緩存
conn.setRequestProperty("Cache-Control", "no-cache");
conn.setRequestProperty("Connection", "Close");// 不支持Keep-Alive
conn.setUseCaches(false);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setInstanceFollowRedirects(true);
conn.setRequestMethod(method);
conn.setConnectTimeout(8000);
conn.setReadTimeout(8000);
return conn;
}
/**
* 根據url
*
* @param url 請求路徑
* @return isFile
* @throws IOException 異常
*/
//待改進
protected static HttpURLConnection getConnection(URL url, boolean isFile) throws IOException {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
if (conn == null) {
throw new IOException("connection can not be null");
}
//設置從httpUrlConnection讀入
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
//如果是上傳文件,則設爲POST
if (isFile) {
conn.setRequestMethod(POST_METHOD); //GET和 POST都可以 文件略大改成POST
}
// 設置請求頭信息
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Charset", String.valueOf(StandardCharsets.UTF_8));
conn.setConnectTimeout(8000);
conn.setReadTimeout(8000);
return conn;
}
/**
* 拼接參數
*
* @param url 需要拼接參數的url
* @param map 參數
* @param charset 編碼格式
* @return 拼接完成後的url
*/
public static String setParmas(String url, Map<String, String> map, String charset) throws Exception {
String result = StringUtils.EMPTY;
boolean hasParams = false;
if (StringUtils.isNotEmpty(url) && MapUtils.isNotEmpty(map)) {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey().trim();
String value = entry.getValue().trim();
if (hasParams) {
builder.append("&");
} else {
hasParams = true;
}
if (StringUtils.isNotEmpty(charset)) {
builder.append(key).append("=").append(URLEncoder.encode(value, charset));
} else {
builder.append(key).append("=").append(value);
}
}
result = builder.toString();
}
URL u = new URL(url);
if (StringUtils.isEmpty(u.getQuery())) {
if (url.endsWith("?")) {
url += result;
} else {
url = url + "?" + result;
}
} else {
if (url.endsWith("&")) {
url += result;
} else {
url = url + "&" + result;
}
}
logger.debug("request url is {}", url);
return url;
}
}
遇到的問題
1)"errcode":40001,"errmsg":"invalid credential, access_token is invalid or not latest hint: [Ua2IXa0080sz47!]"
獲取的access_token不對,這邊的token不是授權的token,是公衆號調用各接口時使用的access_token