From ff8f30bee1bd55e968ddd1f64c89b265e94a02df Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Tue, 21 Jul 2026 13:21:48 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=AE=E7=AD=94=E5=B0=8F=E5=8A=A9=E6=89=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/customer-report-wecom-callback.md | 12 ++-- .../config/FoundationProperties.java | 31 ++++++--- .../integration/wecom/client/WecomClient.java | 65 +++++++++---------- .../controller/WecomCallbackController.java | 51 +++++++++++++-- src/main/resources/application.yml | 15 ++++- 5 files changed, 119 insertions(+), 55 deletions(-) diff --git a/docs/customer-report-wecom-callback.md b/docs/customer-report-wecom-callback.md index b07585d..abdb825 100644 --- a/docs/customer-report-wecom-callback.md +++ b/docs/customer-report-wecom-callback.md @@ -1,7 +1,10 @@ # 客资日报企业微信回调 -客资日报助手复用现有企业微信通用配置,不新增第二套 CorpId、AgentId、Secret、Token 或 EncodingAESKey。 -每个助手只配置自己的回调地址,服务端使用独立的回调方法接收消息: +同一企业下的助手共用 CorpId;每个自建应用分别配置 AgentId、Secret 和回调地址。运动馆智能助手使用: + +`GET/POST /v1/integrations/wecom/sports-venue/callback` + +原客资日报地址继续作为兼容入口: `GET/POST /v1/integrations/wecom/customer-report/callback` @@ -9,8 +12,9 @@ GET 用于企业微信保存回调配置时验签,POST 用于接收用户消 部署前配置: -- 沿用 `foundation.wecom` 下现有的 CorpId、AgentId、Secret、Token、EncodingAESKey。 +- 沿用 `foundation.wecom` 下现有的 CorpId、Token、EncodingAESKey。 +- 问答小助手和运动馆智能助手分别配置在 `foundation.wecom.apps.general`、`foundation.wecom.apps.sports-venue`。 - `DEEPSEEK_API_KEY`(沿用项目现有 DeepSeek 配置)。 - `CRM_REPORT_BASE_URL`,指向学邦同步服务,例如 `http://127.0.0.1:9999`。 -`/v1/integrations/wecom/callback` 和 `/v1/integrations/wecom/customer-report/callback` 分别对应独立的 GET/POST 回调方法,但复用签名验证、解密和语义路由逻辑。后续增加助手时只新增回调路径和入口方法,不重复企业微信公共配置。 +`/v1/integrations/wecom/callback` 保持原有扣子默认工作流;`/v1/integrations/wecom/sports-venue/callback` 使用 DeepSeek 语义识别并调用客资日报接口。回复消息时,服务端会根据入口选择对应应用的 AgentId 和 Secret。 diff --git a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java index 431f7b9..f42c3a7 100644 --- a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java +++ b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java @@ -100,29 +100,42 @@ public class FoundationProperties { public static class Wecom { private boolean enabled; private String corpId; - private Integer agentId; - private String secret; + private String defaultApp = "general"; private String studentContactSecret; private String token; private String encodingAesKey; - private String callbackPath = "/v1/integrations/wecom/callback"; + private Map apps = new HashMap<>(); public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getCorpId() { return corpId; } public void setCorpId(String corpId) { this.corpId = corpId; } - public Integer getAgentId() { return agentId; } - public void setAgentId(Integer agentId) { this.agentId = agentId; } - public String getSecret() { return secret; } - public void setSecret(String secret) { this.secret = secret; } + public String getDefaultApp() { return defaultApp; } + public void setDefaultApp(String defaultApp) { this.defaultApp = defaultApp; } public String getStudentContactSecret() { return studentContactSecret; } public void setStudentContactSecret(String studentContactSecret) { this.studentContactSecret = studentContactSecret; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getEncodingAesKey() { return encodingAesKey; } public void setEncodingAesKey(String encodingAesKey) { this.encodingAesKey = encodingAesKey; } - public String getCallbackPath() { return callbackPath; } - public void setCallbackPath(String callbackPath) { this.callbackPath = callbackPath; } + public Map getApps() { return apps; } + public void setApps(Map apps) { this.apps = apps; } + + public static class App { + private String name; + private Integer agentId; + private String secret; + private String callbackPath; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Integer getAgentId() { return agentId; } + public void setAgentId(Integer agentId) { this.agentId = agentId; } + public String getSecret() { return secret; } + public void setSecret(String secret) { this.secret = secret; } + public String getCallbackPath() { return callbackPath; } + public void setCallbackPath(String callbackPath) { this.callbackPath = callbackPath; } + } } public static class Semantic { diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java b/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java index 045f7ab..8150a9b 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java @@ -27,8 +27,6 @@ public class WecomClient { private final FoundationProperties properties; private final WebClient webClient; - private volatile String cachedToken; - private volatile Instant tokenExpireAt = Instant.EPOCH; private final Map tokenCaches = new ConcurrentHashMap<>(); public WecomClient(FoundationProperties properties, WebClient.Builder builder) { @@ -37,11 +35,17 @@ public class WecomClient { } public void sendText(String toUser, String content) { - log.info("wecom sendText start toUser={} agentId={} contentPreview={}", toUser, properties.getWecom().getAgentId(), preview(content)); - String token = getAccessToken(); + sendText(properties.getWecom().getDefaultApp(), toUser, content); + } + + public void sendText(String appKey, String toUser, String content) { + FoundationProperties.Wecom.App app = app(appKey); + log.info("wecom sendText start appKey={} toUser={} agentId={} contentPreview={}", + appKey, toUser, app.getAgentId(), preview(content)); + String token = getAccessToken(app.getSecret()); WecomSendMessageRequest req = new WecomSendMessageRequest(); req.setToUser(toUser); - req.setAgentId(properties.getWecom().getAgentId()); + req.setAgentId(app.getAgentId()); WecomSendMessageRequest.Text text = new WecomSendMessageRequest.Text(); text.setContent(content); req.setText(text); @@ -118,35 +122,8 @@ public class WecomClient { return response; } - private String getAccessToken() { - if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) { - log.info("wecom gettoken use cached token expireAt={}", tokenExpireAt); - return cachedToken; - } - - WecomTokenResponse response = webClient.get() - .uri(uriBuilder -> uriBuilder.path("/cgi-bin/gettoken") - .queryParam("corpid", properties.getWecom().getCorpId()) - .queryParam("corpsecret", properties.getWecom().getSecret()) - .build()) - .retrieve() - .bodyToMono(WecomTokenResponse.class) - .block(); - - if (response == null || !response.success() || response.accessToken() == null) { - log.error("wecom gettoken failed err={}", response == null ? "null" : response.errmsg()); - throw new IllegalStateException("WeCom gettoken failed: " + (response == null ? "null" : response.errmsg())); - } - - cachedToken = response.accessToken(); - int expires = response.expiresIn() == null ? 7200 : response.expiresIn(); - tokenExpireAt = Instant.now().plusSeconds(Math.max(300, expires - 120)); - log.info("wecom gettoken success expireAt={}", tokenExpireAt); - return cachedToken; - } - private String getAccessToken(String secret) { - String safeSecret = secret == null || secret.isBlank() ? properties.getWecom().getSecret() : secret.trim(); + String safeSecret = requireText(secret, "WeCom app secret is missing"); TokenCache cache = tokenCaches.computeIfAbsent(safeSecret, ignored -> new TokenCache()); if (cache.token != null && Instant.now().isBefore(cache.expireAt)) { log.info("wecom gettoken use cached token expireAt={}", cache.expireAt); @@ -176,7 +153,27 @@ public class WecomClient { private String studentContactSecret() { String secret = properties.getWecom().getStudentContactSecret(); - return secret == null || secret.isBlank() ? properties.getWecom().getSecret() : secret; + return secret == null || secret.isBlank() ? app(properties.getWecom().getDefaultApp()).getSecret() : secret; + } + + private FoundationProperties.Wecom.App app(String appKey) { + String safeKey = requireText(appKey, "WeCom app key is missing"); + FoundationProperties.Wecom.App app = properties.getWecom().getApps().get(safeKey); + if (app == null) { + throw new IllegalStateException("WeCom app is not configured: " + safeKey); + } + if (app.getAgentId() == null || app.getAgentId() <= 0) { + throw new IllegalStateException("WeCom agent-id is missing: " + safeKey); + } + requireText(app.getSecret(), "WeCom secret is missing: " + safeKey); + return app; + } + + private String requireText(String value, String message) { + if (value == null || value.isBlank()) { + throw new IllegalStateException(message); + } + return value.trim(); } private String preview(String text) { diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java index b8a07ea..b3a54e5 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java @@ -1,5 +1,6 @@ package com.company.agentfoundation.integration.wecom.controller; +import com.company.agentfoundation.integration.coze.service.CozeWorkflowService; import com.company.agentfoundation.integration.wecom.client.WecomClient; import com.company.agentfoundation.integration.wecom.model.WecomEncryptedEnvelope; import com.company.agentfoundation.integration.wecom.model.WecomIncomingMessage; @@ -27,7 +28,10 @@ public class WecomCallbackController { private static final Logger log = LoggerFactory.getLogger(WecomCallbackController.class); private static final long MESSAGE_DEDUP_TTL_SECONDS = 600; private static final String ERROR_REPLY = "语义识别或业务查询失败,请稍后重试"; + private static final String GENERAL_APP = "general"; + private static final String SPORTS_VENUE_APP = "sports-venue"; private final SemanticDispatchService semanticDispatchService; + private final CozeWorkflowService cozeWorkflowService; private final WecomClient wecomClient; private final WecomCryptoService wecomCryptoService; private final XmlMapper xmlMapper = new XmlMapper(); @@ -35,10 +39,12 @@ public class WecomCallbackController { public WecomCallbackController( SemanticDispatchService semanticDispatchService, + CozeWorkflowService cozeWorkflowService, WecomClient wecomClient, WecomCryptoService wecomCryptoService ) { this.semanticDispatchService = semanticDispatchService; + this.cozeWorkflowService = cozeWorkflowService; this.wecomClient = wecomClient; this.wecomCryptoService = wecomCryptoService; } @@ -82,7 +88,27 @@ public class WecomCallbackController { @RequestParam("nonce") String nonce, @RequestBody String body ) throws Exception { - return receiveCallback(signature, timestamp, nonce, body, "default"); + return receiveCallback(signature, timestamp, nonce, body, "default", GENERAL_APP, CallbackMode.GENERAL_CHAT); + } + + @GetMapping("/sports-venue/callback") + public String verifySportsVenueCallback( + @RequestParam("msg_signature") String signature, + @RequestParam("timestamp") String timestamp, + @RequestParam("nonce") String nonce, + @RequestParam("echostr") String echostr + ) { + return verifyCallback(signature, timestamp, nonce, echostr, "sports-venue"); + } + + @PostMapping(value = "/sports-venue/callback", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE}) + public String receiveSportsVenueCallback( + @RequestParam("msg_signature") String signature, + @RequestParam("timestamp") String timestamp, + @RequestParam("nonce") String nonce, + @RequestBody String body + ) throws Exception { + return receiveCallback(signature, timestamp, nonce, body, "sports-venue", SPORTS_VENUE_APP, CallbackMode.SEMANTIC_ROUTING); } @PostMapping(value = "/customer-report/callback", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE}) @@ -92,10 +118,18 @@ public class WecomCallbackController { @RequestParam("nonce") String nonce, @RequestBody String body ) throws Exception { - return receiveCallback(signature, timestamp, nonce, body, "customer-report"); + return receiveCallback(signature, timestamp, nonce, body, "customer-report", SPORTS_VENUE_APP, CallbackMode.SEMANTIC_ROUTING); } - private String receiveCallback(String signature, String timestamp, String nonce, String body, String callbackName) throws Exception { + private String receiveCallback( + String signature, + String timestamp, + String nonce, + String body, + String callbackName, + String appKey, + CallbackMode callbackMode + ) throws Exception { log.info("wecom callback route={} message received", callbackName); log.info("wecom callback message received safeMode={} timestamp={} nonce={} bodyLength={}", wecomCryptoService.isSafeModeEnabled(), timestamp, nonce, body == null ? 0 : body.length()); @@ -124,7 +158,9 @@ public class WecomCallbackController { String reply; try { - reply = semanticDispatchService.dispatch(msg.getFromUserName(), userText); + reply = callbackMode == CallbackMode.GENERAL_CHAT + ? cozeWorkflowService.chatDefault(userText) + : semanticDispatchService.dispatch(msg.getFromUserName(), userText); } catch (Exception e) { log.error("wecom callback semantic dispatch failed fromUser={} err={}", msg.getFromUserName(), e.getMessage(), e); reply = ERROR_REPLY; @@ -134,7 +170,7 @@ public class WecomCallbackController { reply = ERROR_REPLY; } log.info("wecom callback sending semantic reply toUser={} replyPreview={}", msg.getFromUserName(), preview(reply)); - wecomClient.sendText(msg.getFromUserName(), reply); + wecomClient.sendText(appKey, msg.getFromUserName(), reply); log.info("wecom callback reply sent toUser={}", msg.getFromUserName()); return "success"; } @@ -192,4 +228,9 @@ public class WecomCallbackController { } return text.length() <= 80 ? text : text.substring(0, 80) + "..."; } + + private enum CallbackMode { + GENERAL_CHAT, + SEMANTIC_ROUTING + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8dd105a..2c1ce6b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -50,11 +50,20 @@ foundation: wecom: enabled: true corp-id: wwaec174f56094f2b5 - agent-id: 1000003 - secret: Sq76WOBNf3S85RoSzrJ_3u5iQrl8DRTvjTv9X0-z7HE + default-app: general token: crmAgentToken2026 encoding-aes-key: k3F8mN2vQ7xLp9sTd4YwH6cRb1JuEz5AaZq7BnVcK2D - callback-path: /v1/integrations/wecom/callback + apps: + general: + name: 问答小助手 + agent-id: 1000003 + secret: Sq76WOBNf3S85RoSzrJ_3u5iQrl8DRTvjTv9X0-z7HE + callback-path: /v1/integrations/wecom/callback + sports-venue: + name: 运动馆智能助手 + agent-id: 1000006 + secret: V-fh1ROzjb06Hi9XB8VPb4HhE2ApI4j99NAjAXKYOb8 + callback-path: /v1/integrations/wecom/sports-venue/callback semantic: agent-id: crm-assistant minimum-confidence: 0.60