1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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;
    }
 
}