管理系统必备技(13):Java对接微信公众号实现消息交互

1,558 阅读4分钟

我正在参加「掘金·启航计划」

前言

本文是一次关于微信公众号开发的实践,将微信公众号的人员与平台的用户进行一个绑定。

已知,平台的用户字段有手机号、公众号用户id。而关注公众号后能获取到的用户信息中主要有如下参数,而无法获取到用户的手机号。因此,要想实现绑定有两种方式:

  • 1、通过公众号开放手机号绑定入口;
  • 2、通过小程序与微信开放平台进行平台用户关联。

第二种中,需要用户登录小程序,在小程序调用API来获取用户的手机号,最后通过 unionId 来实现小程序和公众号及平台用户的绑定。但目前是这批用户没有必要关注公众号的需求。因此,本文采用方式一进行实践。

一、准备工作

1、配置好微信公众号环境。

将服务器地址改为服务的接收地址。通过nginx转发到目标服务。(服务器回调地址需要公网地址,否则微信无法调通)

2、生成你的令牌 token

这部就生成后保存到 redis中,然后用的时候取出来即可,token的有效期为 2小时,所以到期后需要对redis 做配置,然后写程序自动续签 token。

3、编写程序接收服务消息

接收服务一共需要两个接口,这两个接口路由完全一样,一个为 Get 请求,一个为 Post 请求。

  • 接收 url 校验
    @GetMapping
    public String index(HttpServletRequest request){
        try {
            String signature = request.getParameter("signature");
            String timestamp = request.getParameter("timestamp");
            String nonce = request.getParameter("nonce");
            String echostr = request.getParameter("echostr");
            log.debug("signature = {}",signature);
            log.debug("timestamp = {}",timestamp);
            log.debug("nonce = {}",nonce);
            log.debug("echostr = {}",echostr);
            if(WxCheck.token(signature,timestamp,nonce,token)){
                return echostr;
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }
// 微信检查类
public class WxCheck {

    public static boolean token(String signature,String timestamp,String nonce,String token){
        String[] str = {token, timestamp, nonce};
        // 字典序排序
        Arrays.sort(str);
        String bigStr = str[0] + str[1] + str[2];
        // SHA1加密 hutool 的方法
        String digest = SecureUtil.sha1(bigStr).toLowerCase();
        return digest.equals(signature);
    }

}
  • 接收 post 请求
  /**
     * 接收回调事件
     *
     * @param request
     * @return java.lang.String
     * @author big uncle
     * @date 2021/7/5 18:04
     **/
    @PostMapping
    public String notification(HttpServletRequest request, HttpServletResponse response){
        try{
            String xml = IoUtil.read(request.getInputStream(), Charset.forName("UTF-8"));
            JSONObject jsonObject = XML.toJSONObject(xml);
            jsonObject = jsonObject.getJSONObject("xml");
            String type = jsonObject.getStr("MsgType");
            String event = jsonObject.getStr("Event");
            String eventKey = jsonObject.getStr("EventKey");
            log.debug("得到数据 {}",jsonObject);
            NotificationTextDTO wxn = jsonObject.toBean(NotificationTextDTO.class);
            if(PublicNumberConst.MsgType.MSGTYPE_0.equals(type)){
                rabbitProducer.produce(exchange,PublicNumberConst.MsgType.MSGTYPE_0,objectMapper.writeValueAsString(wxn));
            }
            if(PublicNumberConst.MsgType.MSGTYPE_1.equals(type)){
                rabbitProducer.produce(exchange,PublicNumberConst.MsgType.MSGTYPE_1,objectMapper.writeValueAsString(wxn));
            }
            if(PublicNumberConst.MsgType.MSGTYPE_2.equals(type)){
                rabbitProducer.produce(exchange,PublicNumberConst.MsgType.MSGTYPE_2,objectMapper.writeValueAsString(wxn));
            }
            if(PublicNumberConst.MsgType.MSGTYPE_3.equals(type) || PublicNumberConst.MsgType.MSGTYPE_4.equals(type)){
                rabbitProducer.produce(exchange,PublicNumberConst.MsgType.MSGTYPE_3,objectMapper.writeValueAsString(wxn));
            }
            if(PublicNumberConst.MsgType.MSGTYPE_5.equals(type)){
                rabbitProducer.produce(exchange,PublicNumberConst.MsgType.MSGTYPE_5,objectMapper.writeValueAsString(wxn));
            }
            if(PublicNumberConst.MsgType.MSGTYPE_6.equals(type)){
                if((PublicNumberConst.CustomeEvent.CUSTOM_MENU_PHONE.equals(eventKey))){
                    //手机号事件
                    execPhoneBind(wxn);
                }else{
                    rabbitProducer.produce(exchange,PublicNumberConst.MsgType.MSGTYPE_6,objectMapper.writeValueAsString(wxn));
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }

二、流程代码

首先,需要用户关注后,点击菜单后,调用回调事件,发送用户消息。

然后用户根据消息,做出绑定手机号的输入。

用户接收到手机号好做绑定,返回成功或失败的提醒。

2.1 菜单配置

调用菜单创建接口。

https://api.weixin.qq.com/cgi-bin/menu/create?access_token=

创建目标菜单,例如我的手机号样式可以设计成这样,type 为点击事件,key 为 eventKey。这个key 与后台监听消费的key一致。

 {
      "name": "我的工作",
      "sub_button": [
              {
                  "type": "click",
                  "name": "绑定手机号",
                  "key": "bindPhone"
              }
          ]
}

2.2 发送消息 SDK 工具

调用客服信息。

  • controller 层
    /**
     * 发送客服文本消息
     * @param publicDTO
     * @return
     */
    @PostMapping("/sendMsg")
    public PublicVO sendTextMsg(@RequestBody PublicDTO<TestMsgDTO> publicDTO){
        PublicVO publicVO = MessageSDK.sendMsg(publicDTO);
        return publicVO;
    }
  • model
公共接收类
public class PublicDTO<T> {

    private String token;

    private T data;

}
// 公共返回类
@Data
public class PublicVO {

    private String errcode;

    private String errmsg;

}
  • sdk
public class MessageSDK {
    public static PublicVO sendMsg(PublicDTO<TestMsgDTO> publicDTO) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            String result = HttpRequest
                    .post(String.format(MessageEnum.MESSAGE_TEXT_SEND.getUrl(),publicDTO.getToken()))
                    .body(objectMapper.writeValueAsString(publicDTO.getData()))
                    // 超时
                    .timeout(5000)
                    .execute()
                    .body();
            return objectMapper.readValue(result, PublicVO.class);
        }catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }
}
  • enum 类
public enum MessageEnum {

    /**
     * 文本消息
     **/
    MESSAGE_TEXT_SEND("发送", "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=%s"),
    ;
    /**
     * 描述
     **/
    private String desc;
    /**
     * 地址
     **/
    private String url;

    MessageEnum(String desc, String url) {
        this.desc = desc;
        this.url = url;
    }
    
    public String getDesc() {
        return desc;
    }

    public String getUrl() {
        return url;
    }

    @Override
    public String toString() {
        return "MenuMenum{" +
                "desc='" + desc + ''' +
                ", url='" + url + ''' +
                '}';
    }
}

2.3 发送消息

由上面的方法进行消息发送

 execPhoneBind(wxn);
   private void execPhoneBind(NotificationTextDTO wxn) {
        PublicDTO<TestMsgDTO> publicDTO = new PublicDTO<>();
        String token = redisSdk.get(RedisCacheKeyConst.STATION_MP_TOKEN);
        TestMsgDTO testMsgDTO = new TestMsgDTO();
        TestMsgDTO.Text text = new TestMsgDTO.Text();
        text.setContent("请回复:绑定+当前微信手机号进行手机号与公众号的消息绑定功能。示例:绑定1388888888");
        testMsgDTO.setTouser(wxn.getFromUserName());
        testMsgDTO.setMsgtype(PublicNumberConst.MsgType.MSGTYPE_0);
        testMsgDTO.setText(text);
        publicDTO.setToken(token);
        publicDTO.setData(testMsgDTO);
        sendTextMsg(publicDTO);
    }

到现在,即可完成点击按钮后,消息的监听回复工作。

2.4 绑定手机号

用户输入手机号后,通过 mq 将消息转发到用户服务,或者直接调用 用户的service 层代码进行消费,同步更新数据库。

由于用户发来的消息可能存在多种情况,我们需要分类进行处理,这个就涉及到返回消息的分发问题,我这边只要一个 手机号事件,所以一个if 就好了。

 try {
            log.info("消费的路由键:",envelope.getRoutingKey());
            log.info("消费的内容类型",properties.getContentType());
            long deliveryTag = envelope.getDeliveryTag();
            //确认消息
            this.getChannel().basicAck(deliveryTag, false);
            String str = new String(body, "UTF-8");
            System.out.println("protect消费的消息体内容:"+str);
            log.info("接收的数据:{}",str);
            NotificationTextDTO wxn = mapper.readValue(str, NotificationTextDTO.class);
            /**
             * 识别文本内容:绑定手机号处理
             */
            if(wxn.getContent().startsWith(finalPhoneBind)){
                String phone = wxn.getContent().substring(finalPhoneBind.length(), wxn.getContent().length());
                try {
                    if(PhoneUtil.isPhone(phone)){
                        String encryptPhone = AESUtil.encrypt(phone, AESUtil.appKey);
                        List<SubUser> users = userMapper.selectUserByPhone(encryptPhone);
                        if(CollectionUtil.isNotEmpty(users)){
                            users.stream().forEach(e -> {
                                e.setMpOpenId(wxn.getFromUserName());
                            });
                            userMapper.updateBatchById(users);
                            // 发送成功消息
                            sendSuccesMsg(wxn, users);
                        }
                    }else{
                        // 发送失败消息
                        sendFailMsg(wxn);
                    }
                }catch (Exception e){
                    log.error("发送异常:"+e);
                    e.printStackTrace();
                    // 发送失败消息
                    sendFailMsg(wxn);
                }
            }
        } catch (JsonProcessingException e) {
            log.error("消费公众号文本消息失败,异常信息:{}",e);
            e.printStackTrace();
        }

方法:

/**
     * 发送失败消息
     * @param wxn
     */
    private void sendFailMsg(NotificationTextDTO wxn) {
        PublicDTO<TestMsgDTO> publicDTO = new PublicDTO<>();
        String token = redisSdk.get(RedisCacheKeyConst.STATION_MP_TOKEN);
        TestMsgDTO testMsgDTO = new TestMsgDTO();
        TestMsgDTO.Text text = new TestMsgDTO.Text();
        text.setContent("绑定失败,可能是格式不对或服务器异常,请联系管理员");
        testMsgDTO.setTouser(wxn.getFromUserName());
        testMsgDTO.setMsgtype(PublicNumberConst.MsgType.MSGTYPE_0);
        testMsgDTO.setText(text);
        publicDTO.setToken(token);
        publicDTO.setData(testMsgDTO);
        MessageSDK.sendMsg(publicDTO);
    }

    /**
     * 发送成功消息
     * @param wxn
     * @param users
     */
    private void sendSuccesMsg(NotificationTextDTO wxn, List<SubUser> users) {
        PublicDTO<TestMsgDTO> publicDTO = new PublicDTO<>();
        String token = redisSdk.get(RedisCacheKeyConst.STATION_MP_TOKEN);
        TestMsgDTO testMsgDTO = new TestMsgDTO();
        TestMsgDTO.Text text = new TestMsgDTO.Text();
        Set<String> set = users.stream().map(SubUser::getAccount).collect(Collectors.toSet());
        text.setContent("绑定成功,您当前手机号绑定的账户有:"+StringUtils.join(set,","));
        testMsgDTO.setTouser(wxn.getFromUserName());
        testMsgDTO.setMsgtype(PublicNumberConst.MsgType.MSGTYPE_0);
        testMsgDTO.setText(text);
        publicDTO.setToken(token);
        publicDTO.setData(testMsgDTO);
        MessageSDK.sendMsg(publicDTO);
    }

测试绑定成功和绑定失败的结果如上。