java实现微信公众平台发送模板消息

最近开发公众号项目,前端采用vue开发,后台使用java开发,由于业务需求,需要实现公众号向用户发送重要的服务通知,提醒工作人员进行业务审核。这时候就需要用到微信平台的模板消息,为了保证用户不受到骚扰,在开发者出现需要主动提醒、通知用户时,才允许开发者在公众平台网站中模板消息库中选择模板,选择后获得模板ID,再根据模板ID向用户主动推送提醒、通知消息。常用的服务场景,如信用卡刷卡通知,商品下单成功、购买成功通知等。

获取template_id(注意:仅微信开放平台同事可获取)

通过向微信公众平台申请模板,来获取模板id,模板消息调用时主要需要模板ID和模板中各参数的赋值内容。请注意:
1.模板中参数内容必须以".DATA"结尾,否则视为保留字;
2.模板保留符号"{{ }}"

下图是在微信测试公众号申请模板
java实现微信公众平台发送模板消息_第1张图片

请求模板消息接口

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";
    }   

java实现微信公众平台发送模板消息_第2张图片
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> data; //data数据

    public static TreeMap item(String value, String color) {
        TreeMap params = new TreeMap();
        params.put("value", value);
        params.put("color", color);
        return params;
    }

    public TreeMap> getData() {
        return data;
    }

    public void setData(TreeMap> 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/";

/**
 * 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";
    TreeMap param1 = new TreeMap();
    param1.put("appid", Constants.APPID);
    param1.put("secret", Constants.APPSECRET);
    param1.put("code", code);
    param1.put("grant_type", "authorization_code");
    String accessTokenObj = HttpUtil.doGet(access_token_url, param1);
    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";
    TreeMap param2 = new TreeMap();
    param2.put("appid", Constants.APPID);
    param2.put("secret", Constants.APPSECRET);
    param2.put("grant_type", "client_credential");
    String tokenObj = HttpUtil.doGet(urlToken, param2);
    JSONObject retToken = JSONObject.fromObject(tokenObj);
    String accessToken = String.valueOf(retToken.get("access_token"));
    logger.info("获取access_token,微信平台接口返回{}", accessToken);

    TreeMap> params = new TreeMap>();
    //根据具体模板参数组装
    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 treeMap = new TreeMap();
    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/"
}

微信平台返回的结果:
图片描述
微信公众号通知消息
java实现微信公众平台发送模板消息_第3张图片
工具类:

 
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 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 map) {
        return HttpDefaultExecute(url, GET_METHOD, map, null);
    }

    public static String httpPost(String url, Map 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 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 map) {
        return HttpsDefaultExecute(url, GET_METHOD, map, null);
    }

    public static String doPost(String url, Map 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 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 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

你可能感兴趣的:(java,微信公众平台,模板,消息,vue.js)