# 前置条件
- 已认证公众号 / 已申请微信测试接口
公众号 -> 设置与开发 -> 基本配置中获取 appid 与 appsecret
微信测试接口同理 - 自己的服务器资源
- 需要微信的接口,获取 access_token、生成带参数二维码、获取二维码图片(GET 请求(请使用 https 协议)https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET)
- 建立数据库绑定表(对服务器中的用户与公众号的绑定)
# maven 插件
下方代码用到的 XmlUtil 与 BeanUtil 需要该插件
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all --> | |
<dependency> | |
<groupId>cn.hutool</groupId> | |
<artifactId>hutool-all</artifactId> | |
<version>5.8.25</version> | |
</dependency> |
# 具体代码
# 三个基础类
绑定类
public class WeChatBindModel extends BaseModel<String> { | |
/** | |
* 网站用户标识 | |
*/ | |
@Column(name = "user_id",columnDefinition = "VARCHAR") | |
private String userId; | |
/** | |
* 公众号用户标识 | |
*/ | |
@Id | |
@Column(name = "open_id",columnDefinition = "VARCHAR") | |
private String openId; | |
/** | |
* 创建时间 | |
*/ | |
@Column(name = "create_time",columnDefinition = "VARCHAR") | |
private String createTime; | |
} |
消息类
@Data | |
public class WechatNotifyRequestVO { | |
private String ToUserName; | |
private String FromUserName; | |
private String CreateTime; | |
private String MsgType; | |
private String MsgId; | |
private String Event; | |
private String EventKey; | |
private String Ticket; | |
private String Content; | |
} |
method 枚举类
public enum RequestMethodEnum { | |
/** get*/ | |
GET(1,"GET"), | |
/** post*/ | |
POST(2,"POST"); | |
private final Integer code; | |
private final String desc; | |
RequestMethodEnum(Integer id, String name){ | |
this.code = id; | |
this.desc = name; | |
} | |
public Integer getCode() { | |
return code; | |
} | |
public String getDesc() { | |
return desc; | |
} | |
} |
# 微信接入服务器的接口
接口
@RequestMapping(value = "") | |
public void checkSignature(@RequestBody String xml, HttpServletRequest request, HttpServletResponse response) throws IOException { | |
String method = request.getMethod(); | |
//get 方法,验证微信配置是否成功 | |
if (RequestMethodEnum.GET.getDesc().equals(method)) { | |
String signature = request.getParameter("signature");/// 微信加密签名 | |
String timestamp = request.getParameter("timestamp");/// 时间戳 | |
String nonce = request.getParameter("nonce"); /// 随机数 | |
String echostr = request.getParameter("echostr"); // 随机字符串 | |
boolean bool = TokenValidator.validate(signature, timestamp, nonce); | |
if(bool){ | |
response.getWriter().print(echostr); | |
} | |
} else { | |
//post 方法,用来接收微信返回的消息 | |
wechatbindService.notifyMsg(xml, response); | |
}} |
验证类 TokenValidator
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.util.Arrays; | |
public class TokenValidator { | |
private static final String TOKEN = "FBuVsYCoPQxbPAFnrwZGRxTIWD"; | |
public static boolean validate(String signature, String timestamp, String nonce) { | |
String[] arr = new String[]{TOKEN, timestamp, nonce}; | |
Arrays.sort(arr); | |
StringBuilder sb = new StringBuilder(); | |
for (String s : arr) { | |
sb.append(s); | |
} String str = sb.toString(); | |
String result = ""; | |
try { | |
MessageDigest md = MessageDigest.getInstance("SHA-1"); | |
byte[] digest = md.digest(str.getBytes()); | |
result = bytesToHex(digest); | |
} catch (NoSuchAlgorithmException e) { | |
e.printStackTrace(); | |
} return result.equals(signature); | |
} | |
private static String bytesToHex(byte[] bytes) { | |
StringBuilder sb = new StringBuilder(); | |
for (byte b : bytes) { | |
String hex = Integer.toHexString(b & 0xFF); | |
if (hex.length() == 1) { | |
sb.append("0"); | |
} | |
sb.append(hex); | |
} return sb.toString(); | |
} | |
} |
# 消息转译与绑定
# 主要思路
- 生成带参数二维码中可以传入场景值数字或者字符串,假设当前服务器的用户标识为字符串,那么可以将对应的场景值传入字符串,也就是 userId (在 scene_str 中传入 id 即可)。
- 生成的二维码在手机扫码后,会对服务器进行传值,将场景值与 openid 同时给到服务器进行绑定 (在 EventKey 中获取场景值)
传参样例
{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "d30dadd6-7941-4759-8bca-87eae8e10437"}}} |
返回样例
<xml><ToUserName><![CDATA[开发者ID]]></ToUserName> | |
<FromUserName><![CDATA[openid]]></FromUserName> | |
<CreateTime>1708652664</CreateTime> | |
<MsgType><![CDATA[event]]></MsgType> | |
<Event><![CDATA[subscribe]]></Event> | |
<EventKey><![CDATA[d30dadd6-7941-4759-8bca-87eae8e10437]]></EventKey> | |
</xml> |
主要方法
@Transactional(rollbackFor = Exception.class) | |
@Override | |
public void notifyMsg(String xml, HttpServletResponse response) throws IOException { | |
log.info("后台接收到公众号发来的数据是:\n{}", xml); | |
// 把微信发送给后端的 xml 消息转成 WechatNotifyRequestVO 对象 | |
Map<String, Object> map = XmlUtil.xmlToMap(xml); | |
WechatNotifyRequestVO notify = BeanUtil.fillBeanWithMap(map, new WechatNotifyRequestVO(), true); | |
// 绑定用户信息,不存在则插入,不匹配则更新 | |
log.info("notify \n{}",notify); | |
boolean isBind = handleBindInfo(notify); | |
// 事件类型,subscribe 表示订阅,unsubscribe 表示取消订阅,null 表示用户发送消息过来 | |
String event = notify.getEvent(); | |
String content = ""; | |
if (StringUtils.equals(event, "subscribe")) { | |
log.info("用户关注公众号了...."); | |
// 欢迎语句 | |
returnResponse(response, notify, "[Party]终于等到你!欢迎你的关注!"); | |
} | |
if (StringUtils.equals(event, "unsubscribe")) { | |
log.info("用户取消公众号的订阅了...."); | |
// 删除所绑定的用户信息 | |
} | |
if (!StringUtils.isBlank(notify.getContent())) { | |
log.info("用户发送消息过来,消息内容是:" + notify.getContent()); | |
// 回复,可以回复表情,蓝色可点击链接文字等 | |
// 收到消息自动回复 | |
returnResponse(response, notify, "/:sun已经收到您的消息~"); | |
} | |
if (isBind) { | |
returnResponse(response, notify, "绑定成功"); | |
}} |
返回微信的方法
public void returnResponse(HttpServletResponse response, WechatNotifyRequestVO wechatNotifyRequestVO, String content) throws IOException { | |
response.setHeader("Content-type", "application/xml"); | |
response.setCharacterEncoding("UTF-8"); | |
response.getWriter().write(getXmlReturnMsg(wechatNotifyRequestVO.getFromUserName(),wechatNotifyRequestVO.getToUserName(),System.currentTimeMillis() / 1000, content)); | |
} |
对返回的 xml 封装成一个方法
public String getXmlReturnMsg(String toUser,String fromUser,Long createTime,String content) { | |
return "<xml>\n" + | |
" <ToUserName><![CDATA["+toUser+"]]></ToUserName>\n" + | |
" <FromUserName><![CDATA["+fromUser+"]]></FromUserName>\n" + | |
" <CreateTime>"+createTime+"</CreateTime>\n" + | |
" <MsgType><![CDATA[text]]></MsgType>\n" + | |
" <Content><![CDATA["+content+"]]></Content>\n" + | |
"</xml>"; | |
} |
对绑定信息的处理
public boolean handleBindInfo(WechatNotifyRequestVO wechatNotifyRequestVO) { | |
// 判断是否是绑定信息 | |
if (wechatNotifyRequestVO.getEvent().equals("SCAN") && wechatNotifyRequestVO.getEventKey() != null) { | |
return saveBindInfo(wechatNotifyRequestVO); | |
} else if (wechatNotifyRequestVO.getEvent().equals("subscribe") && wechatNotifyRequestVO.getEventKey() != null) { | |
return saveBindInfo(wechatNotifyRequestVO); | |
} return false; | |
} | |
public boolean saveBindInfo(WechatNotifyRequestVO wechatNotifyRequestVO) { | |
// 获取二维码参数 | |
String userId = wechatNotifyRequestVO.getEventKey(); | |
// 如果 event 是 SCAN 则进行正则匹配 | |
String pattern = "qrscene_(\\d+)"; | |
Pattern regexPattern = Pattern.compile(pattern); | |
Matcher matcher = regexPattern.matcher(userId); | |
if (matcher.find()) { | |
userId = matcher.group(1); | |
} | |
// 获取 openid | |
String openId = wechatNotifyRequestVO.getFromUserName(); | |
// 获取时间 | |
String time = wechatNotifyRequestVO.getCreateTime(); | |
WeChatBindModel model = new WeChatBindModel(userId, openId, time); | |
WeChatBindModel existingModel = wechatbindRepo.findByOpenId(model.getOpenId()); | |
if (existingModel != null && existingModel.getUserId().equals(userId)) { | |
return false; | |
} else if (existingModel != null) { | |
existingModel.setUserId(model.getUserId()); | |
existingModel.setCreateTime(model.getCreateTime()); | |
wechatbindRepo.save(existingModel); | |
return true; | |
} else { | |
wechatbindRepo.save(model); | |
return true; | |
}} |
参考链接