package cn.iocoder.yudao.module.wxcp.controller.admin.callback; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.util.crypto.SHA1; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; import me.chanjar.weixin.cp.bean.message.WxCpXmlOutTextMessage; import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; import org.springframework.web.bind.annotation.*; /** * 企业微信回调 Controller * * 用于接收企业微信的回调通知,包括 URL 验证和消息/事件推送 * * @author 超级管理员 */ @Tag(name = "管理后台 - 企业微信回调") @RestController @RequestMapping("/wxcp/callback") @Slf4j public class WxCpCallbackController { @Resource private WxCpService wxCpService; /** * GET 请求 — 企业微信回调 URL 验证 * * 企业微信管理后台配置"接收消息"URL 时,会发送 GET 请求验证 URL 有效性 * 需要在验证通过后原样返回 echostr 解密后的内容 */ @GetMapping(produces = "text/plain;charset=utf-8") @Operation(summary = "企业微信回调 URL 验证") @Parameters({ @Parameter(name = "msg_signature", description = "企业微信加密签名"), @Parameter(name = "timestamp", description = "时间戳"), @Parameter(name = "nonce", description = "随机数"), @Parameter(name = "echostr", description = "加密的字符串,需要解密回显") }) public String verifyUrl(@RequestParam("msg_signature") String msgSignature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr) { log.info("[verifyUrl][收到企业微信 URL 验证请求 msg_signature={} timestamp={} nonce={} echostr={}]", msgSignature, timestamp, nonce, echostr); try { // 1. 验证签名 // 注意:SHA1.gen 返回小写十六进制,企业微信下发的 msg_signature 也是小写, // 因此必须用 equalsIgnoreCase 比较;若把传入值转大写再与计算值比较,会永远不相等 String signature = SHA1.gen(wxCpService.getWxCpConfigStorage().getToken(), timestamp, nonce, echostr); if (!signature.equalsIgnoreCase(msgSignature)) { log.error("[verifyUrl][签名验证失败,计算签名={},传入签名={}]", signature, msgSignature); return "error: signature verification failed"; } // 2. 解密 echostr 并返回(使用 WxCpCryptUtil) WxCpCryptUtil cryptUtil = new WxCpCryptUtil(wxCpService.getWxCpConfigStorage()); String plainText = cryptUtil.decrypt(echostr); log.info("[verifyUrl][URL 验证成功]"); return plainText; } catch (Exception e) { log.error("[verifyUrl][URL 验证失败]", e); return "error: " + e.getMessage(); } } /** * POST 请求 — 接收企业微信推送的消息和事件 * * 包括:文本消息、图片消息、进入应用事件、扫码事件等 */ @PostMapping(produces = "application/xml;charset=UTF-8") @Operation(summary = "接收企业微信消息/事件推送") @Parameters({ @Parameter(name = "msg_signature", description = "企业微信加密签名"), @Parameter(name = "timestamp", description = "时间戳"), @Parameter(name = "nonce", description = "随机数"), }) public String handleMessage(HttpServletRequest request, @RequestParam("msg_signature") String msgSignature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestBody String requestBody) { log.info("[handleMessage][收到企业微信消息推送 msg_signature={} timestamp={} nonce={} body={}]", msgSignature, timestamp, nonce, requestBody); try { // 1. 解密消息 WxCpXmlMessage inMessage = WxCpXmlMessage.fromEncryptedXml( requestBody, wxCpService.getWxCpConfigStorage(), timestamp, nonce, msgSignature); if (inMessage == null) { log.warn("[handleMessage][消息解密失败,返回空对象]"); return "error: decrypt failed"; } log.info("[handleMessage][解密成功 msgType={} event={} fromUser={}]", inMessage.getMsgType(), inMessage.getEvent(), inMessage.getFromUserName()); // 2. 根据消息类型处理 WxCpXmlOutMessage outMessage = route(inMessage); // 3. 返回响应 if (outMessage != null) { String response = outMessage.toEncryptedXml(wxCpService.getWxCpConfigStorage()); log.info("[handleMessage][响应消息: {}]", response); return response; } return "success"; } catch (Exception e) { log.error("[handleMessage][处理消息异常]", e); return "success"; // 企业微信要求无论如何都要返回 success } } /** * 路由消息到对应的处理器 * * @param inMessage 解密后的消息 * @return 响应消息(可为 null) */ private WxCpXmlOutMessage route(WxCpXmlMessage inMessage) { // 事件类型 if ("event".equals(inMessage.getMsgType())) { String event = inMessage.getEvent(); if ("enter_agent".equals(event)) { log.info("[route][进入应用] 从用户 {}", inMessage.getFromUserName()); return buildTextReply(inMessage, "欢迎进入应用!"); } else if ("subscribe".equals(event)) { log.info("[route][用户关注]"); return buildTextReply(inMessage, "欢迎关注!"); } else if ("change_contact".equals(event)) { log.info("[route][通讯录变更] 变更类型={}", inMessage.getChangeType()); return null; } else { log.info("[route][未处理事件类型={}]", event); return null; } } // 文本消息 if ("text".equals(inMessage.getMsgType())) { log.info("[route][收到文本消息 from={} content={}]", inMessage.getFromUserName(), inMessage.getContent()); return buildTextReply(inMessage, "已收到您的消息"); } log.info("[route][未处理消息类型={}]", inMessage.getMsgType()); return null; } private WxCpXmlOutMessage buildTextReply(WxCpXmlMessage inMessage, String content) { WxCpXmlOutTextMessage out = new WxCpXmlOutTextMessage(); out.setContent(content); out.setCreateTime(System.currentTimeMillis() / 1000); out.setFromUserName(inMessage.getToUserName()); out.setToUserName(inMessage.getFromUserName()); out.setMsgType("text"); return out; } }