【微信公众号】微信模版消息功能对接

目录

一、微信模版消息

二、模版消息怎么用

‌开通模板消息功能‌

配置模板消息‌

熟悉发送模版消息接口

如何获取微信公众号的openId

发送模版消息

结语


一、微信模版消息

先上一张微信公众号模版消息的截图,直观感受一下什么是模版消息:

类似上面这种通知消息,能够给用户发送某种特定通知,以达到提醒用户的作用。截用微信官方给的模版消息介绍:

二、模版消息怎么用

开通模板消息功能

首先,需要在‌微信公众号平台注册并认证,然后开通模板消息功能。具体步骤包括进入微信公众号平台,查看是否已获取模板消息权限,并在模板消息模块中添加模板。

若在左侧菜单中找不到模版消息,可以在“新功能”菜单中添加

配置模板消息

选择合适的模板后,需要配置模板中的具体信息,如发送内容、跳转链接等。选择好模提交模板消息后,需要经过‌微信官方的审核。审核通过后,才能进行消息的群发。 以下是我申请的一个消息模版

熟悉发送模版消息接口

在模版消息审核通过后,我们就可以着手对接代码功能,准备下发消息;

我们先查看一下微信官方给出的发送模版消息接口:

http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN

post发送入参数据:

      {
           "touser":"OPENID",
           "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
           "url":"http://weixin.qq.com/download",  
           "miniprogram":{
             "appid":"xiaochengxuappid12345",
             "pagepath":"index?foo=bar"
           },
           "client_msg_id":"MSG_000001",
           "data":{

                   "keyword1":{
                       "value":"巧克力"
                   },
                   "keyword2": {
                       "value":"39.8元"
                   },
                   "keyword3": {
                       "value":"2014年9月22日"
                   }
           }
       }

参数说明

touser : 微信公众号的openId(一定是微信公众号的openID,不是小程序的)。

template_id:这个就是我们上面申请到的模版ID,完全复制过来,不要弄错。

appid:所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏)。

pagepath:  跳转到小程序页面的路径。这里有个点要注意,要跳转的小程序必须是发布上线的,调试模式的小程序不支持跳转,会报40165错误。

data: 安装申请的模版参数进行填充,可以参考类目模板消息参数值内容限制说明

如何获取微信公众号的openId

一般来说,模版消息都只会是给已关注了公众号的用户发送,在用户关注公众号时,我们可以通过微信回调接口,保存用户的openId。具体操作步骤:

 1、进入公众号,找到“设置与开发-基本配置”,如下图:

配置完服务器地址后,还需要配置地址对应的IP白名单,有时添加白名单后,需要等待几分到十几分钟后才会生效

注意:在启用了服务器配置后,在公众号中“自动回复”和“自定义菜单功能”全部失效,我们可以通过后台接口进行配置。

参考自定义菜单icon-default.png?t=O83Ahttps://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html

添加我们服务器的接口地址,用于给微信处理事件回调,接口的方法名任意,必须实现GET和POST两种接口:

2、代码接收微信事件回调

 /**
     * 验证只接受微信后台的服务请求
     *     开发者通过检验signature对请求进行校验。
     *     若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功。
     *     验证流程如下:
     *     1、将token timestamp nonce三个参数进行字典序排序;
     *     2、将三个参数字符串拼接成一个字符串进行sha1加密;
     *     3、将加密后的字符串与signature对比,相同则该请求来源于微信。
     */
    @GetMapping( "/official/authenticate/{appKey}")
    public String authenticate(@PathVariable("appKey") String appKey,@RequestParam String signature, @RequestParam String timestamp,
                               @RequestParam String nonce, @RequestParam String echostr) {
        boolean result = checkSignature(signature, timestamp, nonce);
        if (result) {
            return echostr;
        }
        return "FAILURE";
    }
    /**
     *  微信消息响应,通过公众号-基本配置-服务器配置,填写回调地址
     *
     * @param request	请求信息
     * @return  java.lang.String
     */
    @PostMapping( "/official/authenticate/{appKey}")
    public String authenticate(@PathVariable("appKey") String appKey, HttpServletRequest request, HttpServletResponse response){
        return authenticateOfficial(request,appKey);
    }


    public boolean checkSignature(String signature, String timestamp, String nonce) {
        String[] array = new String[]{ "微信公众号配置的Token", timestamp, nonce};
        //先对这三个字符串字典排序,然后拼接
        Arrays.sort(array);
        StringBuilder sb = new StringBuilder();
        for (String s : array) {
            sb.append(s);
        }
        //使用SHA-1算法对拼接字符串加密
        MessageDigest messageDigest;
        String hexStr = null;
        try {
            messageDigest = MessageDigest.getInstance("SHA-1");
            byte[] digest = messageDigest.digest(sb.toString().getBytes());
            //将加密后的字符串转换成16进制字符串
            hexStr = CommonUtils.bytesToHexStr(digest);

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        //返回校验结果
        return signature.equalsIgnoreCase(hexStr);
    }

    @NotNull
    private String authenticateOfficial(HttpServletRequest request,String appKey) {
        val msg = MessageUtil.parseRequest(request);
        // 扫码或关注公众号
        log.info("wechat callback message=[{}]", JsonUtil.toJson(msg));
        //消息类型
        MsgTypeEnum msgTypeEnum = MsgTypeEnum.getByType((String) msg.get("Event"));
        if (null == msgTypeEnum) {
            return StrUtil.EMPTY;
        }
        //根据不同消息类型做不同处理
        try {
            switch (msgTypeEnum) {
                case CLICK:
                    String eventKey = (String) msg.get("EventKey");
                    log.info("菜单点击事件:{}",eventKey);
                    String eventMsg = wechatService.handleClickEvent(msg, appKey);
                    log.info("点击事件{}后回复:{}",eventKey,eventMsg);
                    return eventMsg;
                case EVENT:
                    break;
                case SUBSCRIBE:
                    //处理关注事件
                    subscribeService.doSubscribeEvent(msg, msgTypeEnum,appKey);
                    //关注后发送消息
                    String mapToXml = wechatService.sendSubscribeMessage(msg,appKey);
                    log.info("关注后消息回复:{}",mapToXml);
                    return mapToXml;
                case UNSUBSCRIBE:
                    subscribeService.doSubscribeEvent(msg, msgTypeEnum,appKey);
                    break;
                default:
                    break;
            }
        }catch (Exception e){
            log.error("处理微信公众号请求异常:", e);
        }
        return StrUtil.EMPTY;
    }

在post的方法中处理微信的回调事件,当用户在微信界面点击关注了公众号后,会回调到我们上面的POST接口中,事件为subscribe

解析微信事件代码,此处有点需要注意,如果在springboot工程中开启了防XSS攻击,一定要把回调的post接口设为白名单放过,不然解析不到正确的数据

@Slf4j
public class MessageUtil {
    /**
     * 解析微信公众号回调xml
     * @param request 请求
     * @return
     */
    public static Map<String,Object> parseRequest(HttpServletRequest request){
        InputStream inputStream = null;
        Map<String,Object> map = new HashMap<>();
        SAXReader reader = new SAXReader();
        Document document;
        try {
            inputStream = request.getInputStream();
            //读取输入流获取文档对象
            document = reader.read(inputStream);
            //根据文档对象获取根节点
            Element rootElement = document.getRootElement();
            //获取根所有子节点
            List<Element> elements = rootElement.elements();
            for (Element e:elements) {
                map.put(e.getName(),e.getStringValue());
            }
        } catch (IOException | DocumentException e) {
            log.error("处理微信公众号请求异常:", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ioe) {
                    log.error("关闭inputStream异常:", ioe);
                }
            }
        }
        return map;
    }

}

处理订阅(subscribe),此时就可以解析到微信公众号的openId了。

发送模版消息

在获得了openId后,就可以集成发送模版消息的接口,我们也分以下几个步骤处理:

1、通过微信公众号appIdsecret 获取微信的ACCESS_TOKEN,access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。另外使用该接口也有次数限制默认好像是2000次/天。所以建议最好用缓存处理。

 public String getOfficialAccessToken(String appId, String appSecret) {
       
        String redisKey = RedisKeyBuilder.getOfficialTokenKey(appId);
        log.info("微信公众号access_token redisKey:{}", redisKey);
        Object cachToken = redisUtil.get(redisKey);
        long expire = redisUtil.getExpire(redisKey);
        log.info("微信公众号access_token过期时间:{}", expire);
        log.info("微信公众号access_token:{}", cachToken);
        log.info("微信公众号appid:{}", appId);
        log.info("微信公众号secret:{}", appSecret);
        //判断不存在或者过期,从腾讯服务重新获取
        if (cachToken == null || StringUtils.isEmpty(cachToken.toString())) {
            log.info("获取公众号token");
            String getAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
            try {
                getAccessTokenUrl = getAccessTokenUrl.replace("APPID", appId);
                getAccessTokenUrl = getAccessTokenUrl.replace("APPSECRET", appSecret);
                String data = HttpUtil.get(getAccessTokenUrl);
                JSONObject jsonObject = JSONUtil.parseObj(data);
                log.info("公众号获取token:{}", jsonObject);
                String accessToken = jsonObject.getStr("access_token");
                //过期时间7200s
                Integer expiresTime = jsonObject.getInt("expires_in");
                if (StringUtils.isNotEmpty(accessToken)) {
                    //将token保存到缓存中
                    redisUtil.set(redisKey, accessToken, expiresTime - 60, TimeUnit.SECONDS);
                }
                return accessToken;
            } catch (Exception e) {
                log.error("错误信息:{}", e.getMessage());
            }
        }
        assert cachToken != null;
        return cachToken.toString();
    }

2、封装微信模版消息,调用发送消息接口

  /**
     *
     * @param message 消息体替换对象
     * @param templateId 消息模板ID
     * @param page 小程序调整页地址
     * @param openId 消息接收用户
     */
    public void messageSend(WxMessageBean message, MiniProgram miniMessageBean , String templateId, String page, String openId) {
       miniMessageBean.setAppid(weChatProperties.getMiniAppId());
        boolean notEmpty = StringUtils.isNotEmpty(miniMessageBean.getPagepath());
        miniMessageBean.setUsepath(true);
        if(!notEmpty){
            miniMessageBean.setPagepath("pages/index/index");
        }
        String token = (String) tokenCache.getIfPresent(CACHE_TOKEN_KEY);
        if (token == null) {
            token = this.getAccessToken();
        }
        String url = weChatProperties.getUniformSend() + token;
        // 这里的参数要和下面的Map Key值对应
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("touser", openId);
        jsonObject.put("template_id",templateId);
        jsonObject.put("miniprogram",miniMessageBean);
        jsonObject.put("url", StringUtils.isNotEmpty(page)?page:weChatProperties.getPagePath());
        jsonObject.put("data", message);
        log.info("消息{}", jsonObject);
        ResponseEntity<String> forObject = restTemplate.postForEntity(url, jsonObject, String.class);
        JSONObject object = JSON.parseObject(forObject.getBody());
        Integer errcode = (Integer) object.get("errcode");
        //微信token失效重试
        if(errcode == 40001){
            tokenCache.invalidate(CACHE_TOKEN_KEY);
            messageSend(message, miniMessageBean, templateId, page, openId);
            return;
        }

        if (errcode == 0) {
            log.info("消息推送成功");
        } else if (errcode == 40003) {
            log.error("推送消息的openid错误,openid:{},消息内容:{}", openId, message);
        } else if (errcode == 43004) {
            log.error("该用户未关注公众号,openid:{},消息内容:{}", openId, message);
        } else {
            log.error("消息推送失败:" + errcode);
        }
    }

完成上面所有操作后,程序中没有报错,就可以到手机微信中,查看到刚刚发送的模版消息了。

结语

发送微信模版消息的过程其实并不复杂,主要是我们事先需要熟悉微信的相关文档。不过在其开发过程中会遇到各种的意想不到的状况(坑)。我想在看我上面我梳理的流程后,大家会更清晰些!

另外,在实际场景中,我们是要根据业务需求,把模版消息推送到我们业务中具体的某个/某类人。那我们的业务系统中的用户,怎么和微信公众号用户关联起来呢?欢迎关注下篇...

 最后,最后说明下,上面的代码只是为了说明流程性的东西,可能不能直接正常运行,大家如果在实际开发中遇到问题,欢迎留言,我会第一时间回复。感谢!!!