From 4dd98cfad1584f12e646a6167659d2aaccd5f00c Mon Sep 17 00:00:00 2001 From: van Date: Fri, 10 Apr 2026 00:37:24 +0800 Subject: [PATCH] 1 --- .../controller/WeComActivePushController.java | 41 +++++++++++++++++++ .../WeComApplicationTextPushService.java | 40 ++++++++++++++---- src/main/resources/application.yml | 6 +++ 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/main/java/cn/van333/wxsend/business/controller/WeComActivePushController.java b/src/main/java/cn/van333/wxsend/business/controller/WeComActivePushController.java index f185bf4..8fba0fd 100644 --- a/src/main/java/cn/van333/wxsend/business/controller/WeComActivePushController.java +++ b/src/main/java/cn/van333/wxsend/business/controller/WeComActivePushController.java @@ -16,6 +16,9 @@ import javax.annotation.Resource; * Jarvis 服务端调用的企微应用文本「主动推送」入口(与被动回调 {@link WeComCallbackController} 分离)。 *

* Header: X-WxSend-WeCom-Push-Secret 与 {@code jarvis.wecom.push-secret} 一致 + *

+ * 闲管家订单状态/自动发货等:{@code POST /wecom/goofish-active-push},使用自建应用 + * {@code qywx.app.goofishAgentId},Header {@link #HEADER_GOOFISH_PUSH_SECRET} */ @RestController @RequestMapping("/wecom") @@ -25,9 +28,15 @@ public class WeComActivePushController { public static final String HEADER_PUSH_SECRET = "X-WxSend-WeCom-Push-Secret"; + /** 本地 / ruoyi 调用「闲鱼通知应用」推送时携带,与 {@code jarvis.wecom.goofish-push-secret} 一致 */ + public static final String HEADER_GOOFISH_PUSH_SECRET = "X-WxSend-Goofish-Push-Secret"; + @Value("${jarvis.wecom.push-secret:}") private String pushSecret; + @Value("${jarvis.wecom.goofish-push-secret:}") + private String goofishPushSecret; + @Resource private WeComApplicationTextPushService weComApplicationTextPushService; @@ -52,4 +61,36 @@ public class WeComActivePushController { return R.error("推送失败: " + e.getMessage()); } } + + /** + * 闲鱼订单状态变更、自动发货等:使用企微应用 {@code qywx.app.goofishAgentId}(如 1000013)发文本给成员。 + *

+ * 示例:

+     * curl -X POST http://127.0.0.1:36699/wecom/goofish-active-push \
+     *   -H "Content-Type: application/json" \
+     *   -H "X-WxSend-Goofish-Push-Secret: <与 jarvis.wecom.goofish-push-secret 一致>" \
+     *   -d '{"toUser":"UserID","content":"订单 xxx 状态已更新"}'
+     * 
+ */ + @PostMapping(value = "/goofish-active-push", consumes = MediaType.APPLICATION_JSON_VALUE) + public R goofishActivePush( + @RequestHeader(value = HEADER_GOOFISH_PUSH_SECRET, required = false) String secret, + @RequestBody WeComActivePushRequest body) { + if (!StringUtils.hasText(goofishPushSecret) || !goofishPushSecret.equals(secret)) { + return R.error(403, "拒绝访问"); + } + if (body == null || !StringUtils.hasText(body.getToUser())) { + return R.error(400, "toUser 必填"); + } + if (!StringUtils.hasText(body.getContent())) { + return R.error(400, "content 不能为空"); + } + try { + weComApplicationTextPushService.sendGoofishTextToUser(body.getToUser().trim(), body.getContent()); + return R.ok("sent"); + } catch (Exception e) { + log.warn("企微闲鱼应用推送失败 toUser={} err={}", body.getToUser(), e.toString()); + return R.error("推送失败: " + e.getMessage()); + } + } } diff --git a/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java b/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java index 695b0eb..53487b2 100644 --- a/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java +++ b/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java @@ -44,10 +44,34 @@ public class WeComApplicationTextPushService { @Value("${qywx.app.secret:}") private String corpSecret; + /** 闲管家订单状态/发货通知等:独立自建应用(与默认 qywx.app.agentId 可不同) */ + @Value("${qywx.app.goofishAgentId:}") + private String goofishAgentId; + + @Value("${qywx.app.goofishSecret:}") + private String goofishSecret; + public void sendTextToUser(String toUser, String content) throws Exception { if (StrUtil.isBlank(corpId) || StrUtil.isBlank(corpSecret) || StrUtil.isBlank(agentId)) { throw new IllegalStateException("未配置 qywx.app.corpId / agentId / secret,无法主动发应用消息"); } + sendTextWithAgent(toUser, content, agentId, corpSecret); + } + + /** + * 使用「闲鱼/Goofish」自建应用({@code qywx.app.goofishAgentId}+{@code goofishSecret})下发文本。 + */ + public void sendGoofishTextToUser(String toUser, String content) throws Exception { + if (StrUtil.isBlank(corpId) || StrUtil.isBlank(goofishSecret) || StrUtil.isBlank(goofishAgentId)) { + throw new IllegalStateException("未配置 qywx.app.goofishAgentId / goofishSecret,无法下发闲鱼通知"); + } + sendTextWithAgent(toUser, content, goofishAgentId, goofishSecret); + } + + private void sendTextWithAgent(String toUser, String content, String useAgentId, String useCorpSecret) throws Exception { + if (StrUtil.isBlank(corpId) || StrUtil.isBlank(useCorpSecret) || StrUtil.isBlank(useAgentId)) { + throw new IllegalStateException("corpId / agentId / secret 不能为空"); + } if (StrUtil.isBlank(toUser)) { throw new IllegalArgumentException("toUser 不能为空"); } @@ -56,7 +80,7 @@ public class WeComApplicationTextPushService { text = text.substring(0, CONTENT_MAX) + "…"; } - String token = getAccessToken(); + String token = getAccessToken(useCorpSecret); if (StrUtil.isBlank(token)) { throw new IllegalStateException("获取企微 access_token 失败"); } @@ -65,9 +89,9 @@ public class WeComApplicationTextPushService { jsonMap.put("touser", toUser.trim()); int agentIdInt; try { - agentIdInt = Integer.parseInt(agentId.trim()); + agentIdInt = Integer.parseInt(useAgentId.trim()); } catch (NumberFormatException e) { - throw new IllegalStateException("qywx.app.agentId 非法: " + agentId); + throw new IllegalStateException("agentId 非法: " + useAgentId); } jsonMap.put("agentid", agentIdInt); jsonMap.put("safe", 0); @@ -82,7 +106,7 @@ public class WeComApplicationTextPushService { .timeout(30_000) .execute() .body(); - log.info("企微主动文本下发 toUser={} respSnippet={}", toUser, + log.info("企微主动文本下发 agentId={} toUser={} respSnippet={}", useAgentId, toUser, resp != null && resp.length() > 200 ? resp.substring(0, 200) + "..." : resp); SendRespones sendResp = JSON.parseObject(resp, SendRespones.class); @@ -92,18 +116,18 @@ public class WeComApplicationTextPushService { } } - private String getAccessToken() { - if (StrUtil.isBlank(corpId) || StrUtil.isBlank(corpSecret)) { + private String getAccessToken(String corpSecretForApp) { + if (StrUtil.isBlank(corpId) || StrUtil.isBlank(corpSecretForApp)) { return null; } - String cacheKey = WX_ACCESS_TOKEN + corpSecret; + String cacheKey = WX_ACCESS_TOKEN + corpSecretForApp; String cached = redisCache.getCacheObject(cacheKey); if (StrUtil.isNotEmpty(cached)) { return cached; } Map map = new HashMap<>(); map.put("corpid", corpId.trim()); - map.put("corpsecret", corpSecret.trim()); + map.put("corpsecret", corpSecretForApp.trim()); String jsonStr = JSON.toJSONString(map); for (int i = 0; i < 3; i++) { String responseStr = HttpRequest.post(GET_TOKEN) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0887e57..24ef6ce 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -72,6 +72,8 @@ jarvis: shared-secret: jarvis_wecom_bridge_change_me # Jarvis 调 POST /wecom/active-push 时的 Header,须与 ruoyi jarvis.wecom.push-secret 一致 push-secret: jarvis_wecom_push_change_me + # 本地/ruoyi 调 POST /wecom/goofish-active-push 时 Header: X-WxSend-Goofish-Push-Secret + goofish-push-secret: jarvis_wecom_goofish_push_change_me qywx: webhook: @@ -84,5 +86,9 @@ qywx: agentId: "1000012" # 与 agentId 对应的应用 Secret(企微后台应用管理),用于 access_token + message/send secret: "" + # 闲鱼订单通知自建应用:POST /wecom/goofish-active-push(corpid 仍用上面 corpId) + # 生产建议仅通过环境变量注入,勿将真实 Secret 提交仓库 + goofishAgentId: "1000013" + goofishSecret: "${QYWX_GOOFISH_SECRET:hINY8boNFjY3lypOKbcukE8wYnNS9wnXeonB4iTO1rA}" token: "34NXCtEjkl" encodingAESKey: "bNlE8IhjU34CfXflcBhW3gXPwr8xaEDEhnpUvChvw5i"