From f2f6d02b2f38dbda7fc5e8f2c94ecd6a257e26a7 Mon Sep 17 00:00:00 2001 From: van Date: Wed, 1 Apr 2026 01:57:19 +0800 Subject: [PATCH] 1 --- .../jarvis/WeComInboundController.java | 41 ++++++ .../src/main/resources/application-dev.yml | 3 + .../src/main/resources/application-prod.yml | 2 + .../framework/config/SecurityConfig.java | 2 + .../domain/dto/WeComInboundRequest.java | 53 +++++++ .../ruoyi/jarvis/mapper/SuperAdminMapper.java | 5 + .../jarvis/service/ILogisticsService.java | 5 + .../jarvis/service/IWeComInboundService.java | 14 ++ .../jarvis/service/SuperAdminService.java | 2 + .../service/impl/LogisticsServiceImpl.java | 74 ++++++++++ .../service/impl/SuperAdminServiceImpl.java | 5 + .../service/impl/WeComInboundServiceImpl.java | 133 ++++++++++++++++++ .../mapper/jarvis/SuperAdminMapper.xml | 8 ++ 13 files changed, 347 insertions(+) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WeComInboundController.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WeComInboundRequest.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WeComInboundController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WeComInboundController.java new file mode 100644 index 0000000..515dcf2 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WeComInboundController.java @@ -0,0 +1,41 @@ +package com.ruoyi.web.controller.jarvis; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.jarvis.domain.dto.WeComInboundRequest; +import com.ruoyi.jarvis.service.IWeComInboundService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +/** + * wxSend 企微回调桥接:HTTPS + 共享密钥,无登录态 + */ +@RestController +@RequestMapping("/jarvis/wecom") +public class WeComInboundController { + + public static final String HEADER_SECRET = "X-Jarvis-WeCom-Secret"; + + @Value("${jarvis.wecom.inbound-secret:}") + private String inboundSecret; + + @Resource + private IWeComInboundService weComInboundService; + + @PostMapping("/inbound") + public AjaxResult inbound( + @RequestHeader(value = HEADER_SECRET, required = false) String secret, + @RequestBody WeComInboundRequest body) { + if (!StringUtils.hasText(inboundSecret) || !inboundSecret.equals(secret)) { + return AjaxResult.error("拒绝访问"); + } + String reply = weComInboundService.handleInbound(body != null ? body : new WeComInboundRequest()); + Map data = new HashMap<>(2); + data.put("reply", reply != null ? reply : ""); + return AjaxResult.success(data); + } +} diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 142608e..33c837d 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -205,6 +205,9 @@ jarvis: # 获取评论接口服务地址(后端转发,避免前端跨域) fetch-comments: base-url: http://192.168.8.60:5008 + # 企微经 wxSend 调用本接口时校验(须与 wxSend 配置一致) + wecom: + inbound-secret: jarvis_wecom_bridge_change_me # Ollama 大模型服务(监控健康度调试用) ollama: base-url: http://192.168.8.34:11434 diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 4884542..799f450 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -205,6 +205,8 @@ jarvis: # 获取评论接口服务地址(后端转发) fetch-comments: base-url: http://192.168.8.60:5008 + wecom: + inbound-secret: jarvis_wecom_bridge_change_me # Ollama 大模型服务(监控健康度调试用) ollama: base-url: http://192.168.8.34:11434 diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 64bca2f..a492c23 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -122,6 +122,8 @@ public class SecurityConfig .antMatchers("/kdocs-callback").permitAll() // 旧 WPS 回调路径:重定向到新路径,便于后台仍登记旧 URL 时可用 .antMatchers("/wps365-callback").permitAll() + // 企微消息经 wxSend 转发的桥接(依赖请求头共享密钥) + .antMatchers("/jarvis/wecom/inbound").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WeComInboundRequest.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WeComInboundRequest.java new file mode 100644 index 0000000..c1d3556 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WeComInboundRequest.java @@ -0,0 +1,53 @@ +package com.ruoyi.jarvis.domain.dto; + +/** + * 企微消息经 wxSend 转发至 Jarvis 的请求体 + */ +public class WeComInboundRequest { + private String fromUserName; + private String content; + /** 企微 CorpId */ + private String toUserName; + private String agentId; + private String msgId; + + public String getFromUserName() { + return fromUserName; + } + + public void setFromUserName(String fromUserName) { + this.fromUserName = fromUserName; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getToUserName() { + return toUserName; + } + + public void setToUserName(String toUserName) { + this.toUserName = toUserName; + } + + public String getAgentId() { + return agentId; + } + + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/mapper/SuperAdminMapper.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/mapper/SuperAdminMapper.java index ab3c59f..3ec6857 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/mapper/SuperAdminMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/mapper/SuperAdminMapper.java @@ -65,4 +65,9 @@ public interface SuperAdminMapper * @return 结果 */ public int deleteSuperAdminByIds(Long[] ids); + + /** + * 企微成员 UserID 对应 super_admin.wxid + */ + SuperAdmin selectSuperAdminByWecomUserId(String wxid); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java index 0e46bc9..a86ae34 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java @@ -12,6 +12,11 @@ public interface ILogisticsService { * @return 是否成功获取并推送 */ boolean fetchLogisticsAndPush(JDOrder order); + + /** + * 分享类京东物流短链:查运单并通过 wxts 推送到指定 touser(不依赖订单表) + */ + boolean fetchLogisticsByShareLinkAndPush(String trackingUrl, String remark, String touser); /** * 检查订单是否已处理过(Redis中是否有运单号) diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java new file mode 100644 index 0000000..652d7c6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java @@ -0,0 +1,14 @@ +package com.ruoyi.jarvis.service; + +import com.ruoyi.jarvis.domain.dto.WeComInboundRequest; + +/** + * 企微文本消息业务入口(由 wxSend 通过 HTTPS + 共享密钥调用) + */ +public interface IWeComInboundService { + + /** + * @return 被动回复文本;无内容时返回空串 + */ + String handleInbound(WeComInboundRequest request); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/SuperAdminService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/SuperAdminService.java index 017be73..6485781 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/SuperAdminService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/SuperAdminService.java @@ -59,4 +59,6 @@ public interface SuperAdminService public int deleteSuperAdminById(Long id); SuperAdmin selectSuperAdminByUnionId(Long unionId); + + SuperAdmin selectSuperAdminByWecomUserId(String wxid); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java index 1f54a08..3d3b124 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java @@ -17,10 +17,13 @@ import org.springframework.beans.factory.annotation.Value; import javax.annotation.Resource; import javax.annotation.PostConstruct; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Calendar; import java.util.Date; import java.util.concurrent.TimeUnit; +import org.springframework.util.DigestUtils; + /** * 物流信息服务实现类 */ @@ -31,6 +34,7 @@ public class LogisticsServiceImpl implements ILogisticsService { private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:"; private static final String REDIS_LOCK_KEY_PREFIX = "logistics:lock:order:"; private static final String REDIS_HEALTH_CHECK_ALERT_KEY = "logistics:health:alert:"; + private static final String REDIS_ADHOC_WAYBILL_PREFIX = "logistics:adhoc:waybill:"; private static final String PUSH_URL = "https://wxts.van333.cn/wx/send/pdd"; private static final String PUSH_TOKEN = "super_token_b62190c26"; private static final String CONFIG_KEY_PREFIX = "logistics.push.touser."; @@ -418,6 +422,76 @@ public class LogisticsServiceImpl implements ILogisticsService { } } } + + @Override + public boolean fetchLogisticsByShareLinkAndPush(String trackingUrl, String remark, String touser) { + if (!StringUtils.hasText(trackingUrl)) { + logger.warn("分享物流链接为空"); + return false; + } + String url = trackingUrl.trim(); + String dedupeKey = REDIS_ADHOC_WAYBILL_PREFIX + DigestUtils.md5DigestAsHex(url.getBytes(StandardCharsets.UTF_8)); + try { + ILogisticsService.HealthCheckResult healthResult = checkHealth(); + if (!healthResult.isHealthy()) { + logger.error("物流服务不可用,adhoc 推送跳过: {}", healthResult.getMessage()); + return false; + } + String externalUrl = externalApiUrlTemplate + URLEncoder.encode(url, "UTF-8"); + String result = HttpUtils.sendGet(externalUrl); + if (!StringUtils.hasText(result)) { + logger.warn("物流接口空响应 adhoc"); + return false; + } + JSONObject parsedData; + try { + Object parsed = JSON.parse(result); + if (!(parsed instanceof JSONObject)) { + return false; + } + parsedData = (JSONObject) parsed; + } catch (Exception e) { + logger.warn("物流响应非JSON adhoc: {}", e.getMessage()); + return false; + } + JSONObject dataObj = parsedData.getJSONObject("data"); + if (dataObj == null) { + return false; + } + String waybillNo = dataObj.getString("waybill_no"); + if (!StringUtils.hasText(waybillNo)) { + logger.info("adhoc 暂未返回运单号(可能未发货)"); + return false; + } + waybillNo = waybillNo.trim(); + String existing = stringRedisTemplate.opsForValue().get(dedupeKey); + if (existing != null && existing.equals(waybillNo)) { + logger.info("adhoc 该链接已推送过运单 {}", waybillNo); + return true; + } + StringBuilder pushContent = new StringBuilder(); + pushContent.append("【分享链接物流】\n"); + pushContent.append("备注:").append(remark != null ? remark : "").append("\n"); + pushContent.append("原链接:").append(url).append("\n"); + pushContent.append("运单号:\n\n\n\n").append(waybillNo).append("\n"); + JSONObject pushParam = new JSONObject(); + pushParam.put("title", "JD物流(分享链接)"); + pushParam.put("text", pushContent.toString()); + if (StringUtils.hasText(touser)) { + pushParam.put("touser", touser.trim()); + } + String pushResult = sendPostWithHeaders(PUSH_URL, pushParam.toJSONString(), PUSH_TOKEN); + boolean success = isPushResponseSuccess(pushResult); + if (success) { + stringRedisTemplate.opsForValue().set(dedupeKey, waybillNo, 30, TimeUnit.DAYS); + logger.info("adhoc 物流推送成功 waybill={}", waybillNo); + } + return success; + } catch (Exception e) { + logger.error("adhoc 物流推送异常", e); + return false; + } + } /** * 调用企业应用推送逻辑 diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SuperAdminServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SuperAdminServiceImpl.java index db47845..2ed2c24 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SuperAdminServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SuperAdminServiceImpl.java @@ -94,4 +94,9 @@ public class SuperAdminServiceImpl implements SuperAdminService public SuperAdmin selectSuperAdminByUnionId(Long unionId) { return superAdminMapper.selectSuperAdminByUnionId(unionId); } + + @Override + public SuperAdmin selectSuperAdminByWecomUserId(String wxid) { + return superAdminMapper.selectSuperAdminByWecomUserId(wxid); + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java new file mode 100644 index 0000000..08e0ab6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java @@ -0,0 +1,133 @@ +package com.ruoyi.jarvis.service.impl; + +import com.ruoyi.jarvis.domain.SuperAdmin; +import com.ruoyi.jarvis.domain.dto.WeComInboundRequest; +import com.ruoyi.jarvis.service.IInstructionService; +import com.ruoyi.jarvis.service.ILogisticsService; +import com.ruoyi.jarvis.service.IWeComInboundService; +import com.ruoyi.jarvis.service.SuperAdminService; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * LinPingFan:全部指令;其他人员:须 super_admin.wxid=企微UserID,且仅「京*」指令 + 京东分享物流链接流程 + */ +@Service +public class WeComInboundServiceImpl implements IWeComInboundService { + + public static final String WE_COM_SUPER_USER_ID = "LinPingFan"; + + private static final String REDIS_PENDING = "jarvis:wecom:jdlog:wait:"; + private static final int PENDING_TTL_HOURS = 24; + private static final Pattern JD_3CN = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+"); + private static final int REPLY_MAX_LEN = 3500; + + @Resource + private SuperAdminService superAdminService; + @Resource + private IInstructionService instructionService; + @Resource + private ILogisticsService logisticsService; + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Override + public String handleInbound(WeComInboundRequest req) { + if (req == null || !StringUtils.hasText(req.getFromUserName())) { + return ""; + } + String from = req.getFromUserName().trim(); + String content = req.getContent() != null ? req.getContent() : ""; + + SuperAdmin row = superAdminService.selectSuperAdminByWecomUserId(from); + boolean isSuper = WE_COM_SUPER_USER_ID.equals(from); + if (!isSuper && row == null) { + return "无权限:请在后台「超级管理员」中维护企微 UserID(wxid 字段)"; + } + + String pendingKey = REDIS_PENDING + from; + String pending = stringRedisTemplate.opsForValue().get(pendingKey); + if (StringUtils.hasText(pending)) { + String t = content.trim(); + if ("取消".equals(t) || "取消录入".equals(t)) { + stringRedisTemplate.delete(pendingKey); + return "已取消物流链接录入"; + } + if (isJdShareLogisticsMessage(content)) { + String url = extractJd3cnUrl(content); + if (url != null) { + stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS); + return "收到物流链接 " + url + " ,请输入备注信息"; + } + } + if (t.startsWith("京")) { + stringRedisTemplate.delete(pendingKey); + } else { + stringRedisTemplate.delete(pendingKey); + String touser = resolveTouser(row, isSuper); + boolean ok = logisticsService.fetchLogisticsByShareLinkAndPush(pending, content.trim(), touser); + return ok ? "已查询并推送运单(接收人见超级管理员 touser)" : "物流查询或推送失败,请稍后重试"; + } + } + + if (isJdShareLogisticsMessage(content)) { + String url = extractJd3cnUrl(content); + if (url != null) { + stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS); + return "收到物流链接 " + url + " ,请输入备注信息"; + } + } + + if (!isSuper) { + String cmd = content.trim(); + if (!cmd.startsWith("京")) { + return "当前账号仅支持:以「京」开头的指令,或发送含 3.cn 京东物流分享链接"; + } + } + + List parts = instructionService.execute(content, false, isSuper); + if (parts == null || parts.isEmpty()) { + return ""; + } + String reply = String.join("\n", parts); + if (reply.length() > REPLY_MAX_LEN) { + reply = reply.substring(0, REPLY_MAX_LEN) + "\n...(截断)"; + } + return reply; + } + + private static boolean isJdShareLogisticsMessage(String text) { + if (!StringUtils.hasText(text)) { + return false; + } + return text.contains("https://3.cn/") || text.contains("http://3.cn/"); + } + + private static String extractJd3cnUrl(String text) { + Matcher m = JD_3CN.matcher(text); + if (m.find()) { + return m.group(); + } + return null; + } + + private String resolveTouser(SuperAdmin row, boolean isSuper) { + if (row != null && StringUtils.hasText(row.getTouser())) { + return row.getTouser().trim(); + } + if (isSuper) { + SuperAdmin ping = superAdminService.selectSuperAdminByWecomUserId(WE_COM_SUPER_USER_ID); + if (ping != null && StringUtils.hasText(ping.getTouser())) { + return ping.getTouser().trim(); + } + } + return null; + } +} diff --git a/ruoyi-system/src/main/resources/mapper/jarvis/SuperAdminMapper.xml b/ruoyi-system/src/main/resources/mapper/jarvis/SuperAdminMapper.xml index bda5de5..48c3b23 100644 --- a/ruoyi-system/src/main/resources/mapper/jarvis/SuperAdminMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/jarvis/SuperAdminMapper.xml @@ -43,6 +43,14 @@ where union_id = #{unionId} + + insert into super_admin