|
|
@@ -0,0 +1,237 @@
|
|
|
+package com.fs.speczone.controller;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.fs.speczone.handler.ProgramActionHandler;
|
|
|
+import com.tencent.wework.SpecCallbackSDK;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.function.Function;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 企业微信数据与智能专区 - 统一回调入口
|
|
|
+ *
|
|
|
+ * 所有来自企业微信的请求(URL验证、应用调用、事件回调)都通过此Controller处理。
|
|
|
+ * 由于企业微信会对请求进行加密,所有操作都必须使用 SpecCallbackSDK 进行解密和验签,
|
|
|
+ * 并将返回数据加密后回传。
|
|
|
+ *
|
|
|
+ * 核心流程:
|
|
|
+ * 1. GET /callback → URL 验证(首次配置回调地址时使用)
|
|
|
+ * 2. POST /callback → 接收具体业务调用或事件
|
|
|
+ * - callType=1:应用调用(SCRM 系统通过企微 API 触发)
|
|
|
+ * - callType=2:事件回调(会话存档同意、关键词命中等)
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@RestController
|
|
|
+public class CallbackController {
|
|
|
+
|
|
|
+ // Spring 会自动将所有实现了 ProgramActionHandler 接口的 Bean 注入此列表
|
|
|
+ @Resource
|
|
|
+ private List<ProgramActionHandler> handlerList;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 动作 → 处理器的映射表
|
|
|
+ * key:action 名称(如 "fetch_conversations")
|
|
|
+ * value:对应的处理器实例
|
|
|
+ */
|
|
|
+ private Map<String, ProgramActionHandler> handlerMap;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在 Bean 初始化后,将 handlerList 转换为 handlerMap,
|
|
|
+ * 便于后续根据 action 快速查找处理器。
|
|
|
+ *
|
|
|
+ * 这样设计的好处是新增一个 action 时,无需修改 Controller 代码,
|
|
|
+ * 只需创建一个新的 @Component 类实现 ProgramActionHandler 接口即可。
|
|
|
+ */
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ handlerMap = handlerList.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ ProgramActionHandler::getAction, // key:处理器能处理的 action
|
|
|
+ Function.identity() // value:处理器本身
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== URL 验证(GET) ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 企业微信首次配置回调 URL 时,会发送 GET 请求进行验证。
|
|
|
+ * 验证成功后,URL 才能被保存。
|
|
|
+ *
|
|
|
+ * @param msgSignature 企业微信生成的签名
|
|
|
+ * @param timestamp 时间戳
|
|
|
+ * @param nonce 随机字符串
|
|
|
+ * @param echostr 加密的验证字符串
|
|
|
+ * @return 解密后的 echostr 明文,企业微信确认后通过验证
|
|
|
+ */
|
|
|
+ @GetMapping("/callback")
|
|
|
+ public ResponseEntity<String> verifyUrl(
|
|
|
+ @RequestParam("msg_signature") String msgSignature,
|
|
|
+ @RequestParam("timestamp") String timestamp,
|
|
|
+ @RequestParam("nonce") String nonce,
|
|
|
+ @RequestParam("echostr") String echostr) {
|
|
|
+
|
|
|
+ log.info("收到应用 GET 回调验证");
|
|
|
+
|
|
|
+ // 构造请求头,SDK 需要从 headers 中提取签名信息
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("msg_signature", msgSignature);
|
|
|
+ headers.put("timestamp", timestamp);
|
|
|
+ headers.put("nonce", nonce);
|
|
|
+
|
|
|
+ // 使用 SpecCallbackSDK 进行解析和解密
|
|
|
+ SpecCallbackSDK sdk = new SpecCallbackSDK("GET", headers, echostr);
|
|
|
+ if (!sdk.IsOk()) {
|
|
|
+ log.error("URL 验证解密失败");
|
|
|
+ return ResponseEntity.status(403).body("verify failed");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解密后得到明文 echostr,原样返回即可完成验证
|
|
|
+ return ResponseEntity.ok(sdk.GetData());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 业务入口(POST) ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 接收企业微信的后台调用请求(POST)。
|
|
|
+ * 解密后根据 callType 区分:
|
|
|
+ * callType=1 → 应用调用(同步/异步程序调用)
|
|
|
+ * callType=2 → 事件回调(会话存档同意、关键词命中等)
|
|
|
+ *
|
|
|
+ * @param headers 请求头,包含签名和加密信息
|
|
|
+ * @param body 加密的请求体
|
|
|
+ * @return 加密后的响应
|
|
|
+ */
|
|
|
+ @PostMapping("/callback")
|
|
|
+ public ResponseEntity<String> handleCallback(
|
|
|
+ @RequestHeader Map<String, String> headers,
|
|
|
+ @RequestBody String body) {
|
|
|
+
|
|
|
+ log.info("收到应用 POST 业务请求");
|
|
|
+
|
|
|
+ // 1. 解密和验签
|
|
|
+ SpecCallbackSDK sdk = new SpecCallbackSDK("POST", headers, body);
|
|
|
+ if (!sdk.IsOk()) {
|
|
|
+ log.error("回调验签/解密失败");
|
|
|
+ return ResponseEntity.ok("verify failed");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 提取解密后的数据
|
|
|
+ String decryptedData = sdk.GetData(); // 明文请求体(JSON)
|
|
|
+ long callType = sdk.GetCallType(); // 1: 应用调用, 2: 事件回调
|
|
|
+ String corpid = sdk.GetCorpId(); // 企业 ID
|
|
|
+ long agentId = sdk.GetAgentId(); // 应用 ID
|
|
|
+ log.info("收到回调 corpid={} agentid={} callType={} data={}",
|
|
|
+ corpid, agentId, callType, decryptedData);
|
|
|
+
|
|
|
+ // 3. 根据调用类型分发处理
|
|
|
+ String responsePlain;
|
|
|
+ if (callType == 1) {
|
|
|
+ // 应用调用(SCRM 系统通过企业微信 API 触发的请求)
|
|
|
+ responsePlain = handleProgramCall(decryptedData, sdk);
|
|
|
+ } else if (callType == 2) {
|
|
|
+ // 事件回调(企业微信主动推送的事件)
|
|
|
+ responsePlain = handleEvent(decryptedData);
|
|
|
+ } else {
|
|
|
+ responsePlain = "{}";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 加密响应并返回
|
|
|
+ sdk.BuildResponseHeaderBody(responsePlain);
|
|
|
+ Map<String, String> respHeaders = sdk.GetResponseHeaders();
|
|
|
+ String respBody = sdk.GetResponseBody();
|
|
|
+
|
|
|
+ HttpHeaders httpHeaders = new HttpHeaders();
|
|
|
+ respHeaders.forEach(httpHeaders::add);
|
|
|
+ return new ResponseEntity<>(respBody, httpHeaders, HttpStatus.OK);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 应用调用处理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理来自 SCRM 系统的程序调用(callType=1)。
|
|
|
+ * data 本身即为输入协议(input_protocol),包含 action 和业务参数。
|
|
|
+ *
|
|
|
+ * 通过 action 找到对应的 ProgramActionHandler 并执行。
|
|
|
+ *
|
|
|
+ * @param data 解密后的请求数据(JSON 字符串)
|
|
|
+ * @param sdk SpecCallbackSDK 实例,可获取 ability_id、job_info 等上下文
|
|
|
+ * @return 明文的响应 JSON(会被加密后返回)
|
|
|
+ */
|
|
|
+ private String handleProgramCall(String data, SpecCallbackSDK sdk) {
|
|
|
+ try {
|
|
|
+ JSONObject inputProtocol = JSON.parseObject(data); // 直接解析 request_data
|
|
|
+ String action = inputProtocol.getString("action"); // 提取 action
|
|
|
+
|
|
|
+ ProgramActionHandler handler = handlerMap.get(action);
|
|
|
+ JSONObject output;
|
|
|
+ if (handler != null) {
|
|
|
+ output = handler.handle(inputProtocol, sdk);
|
|
|
+ } else {
|
|
|
+ output = new JSONObject();
|
|
|
+ output.put("errcode", 400);
|
|
|
+ output.put("errmsg", "未知的 action: " + action);
|
|
|
+ }
|
|
|
+ return JSON.toJSONString(output);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理程序调用异常", e);
|
|
|
+ JSONObject err = new JSONObject();
|
|
|
+ err.put("errcode", -1);
|
|
|
+ err.put("errmsg", "内部错误: " + e.getMessage());
|
|
|
+ return err.toJSONString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 事件回调处理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理企业微信推送的事件回调(callType=2)。
|
|
|
+ * 例如:客户同意会话存档、关键词规则命中、新消息产生等。
|
|
|
+ *
|
|
|
+ * 当前只打印日志,实际业务可在此扩展。
|
|
|
+ *
|
|
|
+ * @param data 解密后的事件数据(JSON 字符串)
|
|
|
+ * @return 空 JSON(暂无特殊处理)
|
|
|
+ */
|
|
|
+ private String handleEvent(String data) {
|
|
|
+ try {
|
|
|
+ JSONObject event = JSON.parseObject(data);
|
|
|
+ String eventType = event.getString("event_type");
|
|
|
+
|
|
|
+ // 根据事件类型进行不同处理(可扩展为类似 Handler 的策略模式)
|
|
|
+ if ("keyword_rule_hit".equals(eventType)) {
|
|
|
+ log.info("关键词规则命中事件: {}", event);
|
|
|
+ // TODO: 后续可调用 ConversationService 或通知 SCRM 系统
|
|
|
+ } else if ("chat_record".equals(eventType)) {
|
|
|
+ log.info("会话记录事件: {}", event);
|
|
|
+ // TODO: 后续可进行实时分析、存储等操作
|
|
|
+ } else {
|
|
|
+ log.warn("未处理的事件类型: {}", eventType);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("解析事件失败", e);
|
|
|
+ }
|
|
|
+ return "{}";
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 健康检查 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 健康检查接口,用于确认服务是否正常运行。
|
|
|
+ * Nginx 反向代理或外部监控可通过此端点检测服务状态。
|
|
|
+ */
|
|
|
+ @GetMapping("/health")
|
|
|
+ public ResponseEntity<String> health() {
|
|
|
+ return ResponseEntity.ok("OK");
|
|
|
+ }
|
|
|
+}
|