diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 5f11165..ff2724f 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -214,6 +214,10 @@ jarvis: wxsend-base-url: http://127.0.0.1:36699 # 须与 wxSend jarvis.wecom.push-secret 一致(Header X-WxSend-WeCom-Push-Secret) push-secret: jarvis_wecom_push_change_me + # 闲鱼订单事件 → wxSend POST /wecom/goofish-active-push(须与 wxSend goofish-push-secret 一致) + goofish-push-secret: jarvis_wecom_goofish_push_change_me + # 接收企微通知的成员 UserID,多个逗号或 |;留空则不推送 + goofish-notify-touser: "LinPinFan" # 多轮会话:与 JDUtil interaction_state 类似,TTL 与空闲超时(分钟) session-ttl-minutes: 30 session-idle-timeout-minutes: 30 diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 3ae0518..6036dc5 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -210,6 +210,8 @@ jarvis: inbound-secret: jarvis_wecom_bridge_change_me wxsend-base-url: http://127.0.0.1:36699 push-secret: jarvis_wecom_push_change_me + goofish-push-secret: jarvis_wecom_goofish_push_change_me + goofish-notify-touser: "LinPinFan" session-ttl-minutes: 30 session-idle-timeout-minutes: 30 session-sweep-ms: 60000 diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderChangeLogger.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderChangeLogger.java index 21e0b1e..b978dd9 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderChangeLogger.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderChangeLogger.java @@ -5,6 +5,7 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.jarvis.domain.ErpGoofishOrder; import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog; import com.ruoyi.jarvis.mapper.ErpGoofishOrderEventLogMapper; +import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -30,6 +31,9 @@ public class GoofishOrderChangeLogger { @Resource private ErpGoofishOrderEventLogMapper erpGoofishOrderEventLogMapper; + @Resource + private WxSendGoofishNotifyClient wxSendGoofishNotifyClient; + public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message) { if (orderId == null || StringUtils.isEmpty(message)) { return; @@ -47,8 +51,14 @@ public class GoofishOrderChangeLogger { erpGoofishOrderEventLogMapper.insert(row); } catch (Exception e) { log.warn("闲管家订单事件日志写入失败 orderId={} {}", orderId, e.toString()); + return; } log.info("[goofish-order-event] orderId={} orderNo={} type={} source={} {}", orderId, orderNo, eventType, source, msg); + try { + wxSendGoofishNotifyClient.notifyGoofishEvent(orderNo, eventType, source, msg); + } catch (Exception ex) { + log.debug("闲鱼订单事件 wxSend 通知跳过 err={}", ex.toString()); + } } /** diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/wecom/WxSendGoofishNotifyClient.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/wecom/WxSendGoofishNotifyClient.java new file mode 100644 index 0000000..c027ea1 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/wecom/WxSendGoofishNotifyClient.java @@ -0,0 +1,166 @@ +package com.ruoyi.jarvis.wecom; + +import com.alibaba.fastjson2.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 调用 wxSend 闲鱼自建应用文本推送(POST /wecom/goofish-active-push,agent 见 wxSend qywx.app.goofishAgentId)。 + */ +@Component +public class WxSendGoofishNotifyClient { + + private static final Logger log = LoggerFactory.getLogger(WxSendGoofishNotifyClient.class); + + public static final String HEADER_GOOFISH_PUSH_SECRET = "X-WxSend-Goofish-Push-Secret"; + + /** 与企微 message/send 文本 content 上限对齐,预留余量 */ + private static final int CONTENT_MAX = 2000; + + @Value("${jarvis.wecom.wxsend-base-url:}") + private String wxsendBaseUrl; + + @Value("${jarvis.wecom.goofish-push-secret:}") + private String goofishPushSecret; + + /** + * 接收通知的成员 UserID(企微管理后台可见),多个用英文逗号或 | 分隔; + * 发往 wxSend 时会规范为 touser 的 user1|user2 形式。留空表示不推送。 + */ + @Value("${jarvis.wecom.goofish-notify-touser:}") + private String goofishNotifyTouser; + + /** + * 闲鱼订单事件通知 wxSend(由 wxSend 转企微应用消息)。 + * + * @param orderNo 闲鱼订单号 + * @param eventType ORDER_SYNC / LOGISTICS_SYNC / SHIP + * @param source NOTIFY、LIST、DETAIL_REFRESH 等 + * @param message 已截断的说明文案 + */ + public void notifyGoofishEvent(String orderNo, String eventType, String source, String message) { + if (!StringUtils.hasText(wxsendBaseUrl) || !StringUtils.hasText(goofishPushSecret)) { + return; + } + String toUser = buildTouserParam(goofishNotifyTouser); + if (!StringUtils.hasText(toUser)) { + return; + } + String content = buildContent(orderNo, eventType, source, message); + if (!StringUtils.hasText(content)) { + return; + } + try { + String base = normalizeBase(wxsendBaseUrl); + String url = base + "/wecom/goofish-active-push"; + postJson(url, toUser, content); + } catch (Exception e) { + log.warn("wxSend goofish-active-push 失败 orderNo={} err={}", orderNo, e.toString()); + } + } + + private static String buildContent(String orderNo, String eventType, String source, String message) { + StringBuilder sb = new StringBuilder(); + sb.append("【闲鱼订单】").append(orderNo != null ? orderNo : "-").append("\n"); + if (StringUtils.hasText(eventType)) { + sb.append(eventType); + } + if (StringUtils.hasText(source)) { + sb.append(" | ").append(source); + } + sb.append("\n"); + if (StringUtils.hasText(message)) { + sb.append(message); + } + String s = sb.toString(); + if (s.length() > CONTENT_MAX) { + return s.substring(0, CONTENT_MAX - 1) + "…"; + } + return s; + } + + /** + * 逗号或 | 分隔的 UserID → 企微 API 要求的 user1|user2 + */ + private static String buildTouserParam(String raw) { + if (!StringUtils.hasText(raw)) { + return null; + } + String[] parts = raw.split("[,|]"); + List ids = new ArrayList<>(); + for (String p : parts) { + String t = p == null ? "" : p.trim(); + if (t.length() > 0) { + ids.add(t); + } + } + if (ids.isEmpty()) { + return null; + } + return String.join("|", ids); + } + + private static String normalizeBase(String base) { + String b = base.trim(); + if (b.endsWith("/")) { + return b.substring(0, b.length() - 1); + } + return b; + } + + private void postJson(String url, String toUser, String content) throws Exception { + JSONObject body = new JSONObject(); + body.put("toUser", toUser); + body.put("content", content); + byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8); + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(15000); + conn.setReadTimeout(60000); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + conn.setRequestProperty(HEADER_GOOFISH_PUSH_SECRET, goofishPushSecret); + try (OutputStream os = conn.getOutputStream()) { + os.write(bytes); + } + int code = conn.getResponseCode(); + InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream(); + String resp = readAll(is); + if (code < 200 || code >= 300) { + log.warn("wxSend goofish-active-push HTTP {} body={}", code, resp); + } else { + log.debug("wxSend goofish-active-push OK http={} resp={}", code, resp); + } + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + private static String readAll(InputStream is) throws java.io.IOException { + if (is == null) { + return ""; + } + byte[] buf = new byte[4096]; + StringBuilder sb = new StringBuilder(); + int n; + while ((n = is.read(buf)) >= 0) { + sb.append(new String(buf, 0, n, StandardCharsets.UTF_8)); + } + return sb.toString(); + } +}