我正在参加「掘金·启航计划」
前言
本文是一次关于微信公众号开发的实践,将微信公众号的人员与平台的用户进行一个绑定。
已知,平台的用户字段有手机号、公众号用户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);
}
测试绑定成功和绑定失败的结果如上。