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;
|
}
|
|
}
|