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 2cd2d97..fcd0813 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 @@ -33,6 +33,11 @@ public class GoofishOrderChangeLogger { */ public static final String SOURCE_JD_LOGISTICS_PUSH = "JD_LOGISTICS_PUSH"; + /** + * Redis 写入本地运单:与后续详情运单合并、发货成功通知重复度高,仅落库便于对账,不推企微。 + */ + public static final String SOURCE_REDIS_WAYBILL = "REDIS_WAYBILL"; + @Resource private ErpGoofishOrderEventLogMapper erpGoofishOrderEventLogMapper; @@ -40,6 +45,14 @@ public class GoofishOrderChangeLogger { private WxSendGoofishNotifyClient wxSendGoofishNotifyClient; public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message) { + append(orderId, appKey, orderNo, eventType, source, message, true); + } + + /** + * @param notifyWecom 为 false 时仅写事件日志,不推企微(用于过滤纯对齐类状态)。 + */ + public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message, + boolean notifyWecom) { if (orderId == null || StringUtils.isEmpty(message)) { return; } @@ -59,7 +72,10 @@ public class GoofishOrderChangeLogger { return; } log.info("[goofish-order-event] orderId={} orderNo={} type={} source={} {}", orderId, orderNo, eventType, source, msg); - if (SOURCE_JD_LOGISTICS_PUSH.equals(source)) { + if (SOURCE_JD_LOGISTICS_PUSH.equals(source) || SOURCE_REDIS_WAYBILL.equals(source)) { + return; + } + if (!notifyWecom) { return; } try { @@ -82,16 +98,21 @@ public class GoofishOrderChangeLogger { RowSnap b = RowSnap.from(beforeSnap); RowSnap a = RowSnap.from(afterRow); + boolean refundDiff = !Objects.equals(b.refundStatus, a.refundStatus); + boolean orderDiff = !Objects.equals(b.orderStatus, a.orderStatus); + List orderParts = new ArrayList<>(); - if (!Objects.equals(b.orderStatus, a.orderStatus)) { - orderParts.add("订单状态 " + GoofishStatusLabels.orderStatusChange(b.orderStatus, a.orderStatus)); + if (orderDiff) { + orderParts.add("订单状态 " + GoofishStatusLabels.orderStatusChangeForNotify(b.orderStatus, a.orderStatus)); } - if (!Objects.equals(b.refundStatus, a.refundStatus)) { + if (refundDiff) { orderParts.add("退款状态 " + GoofishStatusLabels.refundStatusChange(b.refundStatus, a.refundStatus)); } if (!orderParts.isEmpty()) { + boolean notifyWecom = refundDiff || (orderDiff + && GoofishStatusLabels.isWxNotifiableOrderStatusChange(b.orderStatus, a.orderStatus)); append(afterRow.getId(), afterRow.getAppKey(), afterRow.getOrderNo(), TYPE_ORDER, source, - String.join(";", orderParts)); + String.join(";", orderParts), notifyWecom); } List logParts = new ArrayList<>(); diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderPipeline.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderPipeline.java index b117f37..a82f366 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderPipeline.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishOrderPipeline.java @@ -143,17 +143,27 @@ public class GoofishOrderPipeline { if (existingBeforeUpdate == null) { goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(), GoofishOrderChangeLogger.TYPE_ORDER, upsertSource, - "新订单入库,订单状态 " + GoofishStatusLabels.orderStatusHuman(loaded.getOrderStatus()) + "新订单入库,订单状态 " + GoofishStatusLabels.orderStatusHumanForNotify(loaded.getOrderStatus()) + ",退款状态 " + GoofishStatusLabels.refundStatusHuman(loaded.getRefundStatus())); return; } - if (!Objects.equals(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus()) - || !Objects.equals(existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus())) { + boolean refundDiff = !Objects.equals(existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus()); + boolean orderDiff = !Objects.equals(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus()); + if (orderDiff || refundDiff) { + List parts = new ArrayList<>(2); + if (orderDiff) { + parts.add("订单状态 " + GoofishStatusLabels.orderStatusChangeForNotify( + existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus())); + } + if (refundDiff) { + parts.add("退款状态 " + GoofishStatusLabels.refundStatusChange( + existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus())); + } + boolean notifyWecom = refundDiff || (orderDiff && GoofishStatusLabels.isWxNotifiableOrderStatusChange( + existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus())); goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(), GoofishOrderChangeLogger.TYPE_ORDER, upsertSource, - "订单状态 " + GoofishStatusLabels.orderStatusChange(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus()) - + ";退款状态 " + GoofishStatusLabels.refundStatusChange( - existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus())); + String.join(";", parts), notifyWecom); } } @@ -425,7 +435,7 @@ public class GoofishOrderPipeline { row.setLocalWaybillNo(wb.trim()); if (goofishOrderChangeLogger != null) { goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(), - GoofishOrderChangeLogger.TYPE_LOGISTICS, "REDIS_WAYBILL", + GoofishOrderChangeLogger.TYPE_LOGISTICS, GoofishOrderChangeLogger.SOURCE_REDIS_WAYBILL, "本地运单 " + (prev == null || prev.isEmpty() ? "无" : prev) + "→" + wb.trim()); } } @@ -532,7 +542,9 @@ public class GoofishOrderPipeline { if (goofishOrderChangeLogger != null) { goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(), GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP", - "发货成功,运单 " + waybill.trim() + ",快递编码 " + expressCode); + "发货成功,运单 " + waybill.trim() + + ",订单状态 " + GoofishStatusLabels.orderStatusHuman(row.getOrderStatus()) + + ",退款状态 " + GoofishStatusLabels.refundStatusHuman(row.getRefundStatus())); } } else { String msg = r != null ? r.getString("msg") : "unknown"; diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishStatusLabels.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishStatusLabels.java index d02cc49..672ff17 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishStatusLabels.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/goofish/GoofishStatusLabels.java @@ -2,6 +2,7 @@ package com.ruoyi.jarvis.service.goofish; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * 闲管家开放平台:订单状态、退款状态中文说明(与 Apifox 订单列表 schema 一致) @@ -79,6 +80,45 @@ public final class GoofishStatusLabels { return orderStatusHuman(from) + " → " + orderStatusHuman(to); } + /** + * 快照/首行展示:待发阶段注明已付款语义(开放平台码 12 即待发货)。 + */ + public static String orderStatusHumanForNotify(Integer s) { + if (Objects.equals(s, 12)) { + return "待发货(已付款)"; + } + return orderStatusHuman(s); + } + + /** + * 企微通知用变化文案:付款完成单独写「已付款(待发货)」。 + */ + public static String orderStatusChangeForNotify(Integer from, Integer to) { + if (Objects.equals(from, 11) && Objects.equals(to, 12)) { + return orderStatusHuman(11) + " → 已付款(待发货)"; + } + return orderStatusHuman(from) + " → " + orderStatusHumanForNotify(to); + } + + /** + * 是否与「付款、待发、在途、终态退款/完成/关闭」相关,从而值得推企微(排除已由 SHIP 覆盖的待发→已发)。 + */ + public static boolean isWxNotifiableOrderStatusChange(Integer from, Integer to) { + if (Objects.equals(from, to)) { + return false; + } + if (Objects.equals(from, 12) && Objects.equals(to, 21)) { + return false; + } + int[] anchors = {11, 12, 21, 22, 23, 24}; + for (int code : anchors) { + if (Objects.equals(from, code) || Objects.equals(to, code)) { + return true; + } + } + return false; + } + public static String refundStatusChange(Integer from, Integer to) { return refundStatusHuman(from) + " → " + refundStatusHuman(to); } 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 index f5e0c21..827a4b1 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/wecom/WxSendGoofishNotifyClient.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/wecom/WxSendGoofishNotifyClient.java @@ -110,22 +110,17 @@ public class WxSendGoofishNotifyClient { } } + /** + * 企微短通知:场景标题 + 订单号 + 必要字段行(不再展示数据来源等冗长前缀)。 + */ private static String buildContent(String orderNo, String eventType, String source, String message) { + String on = orderNo != null ? orderNo : "-"; + String head = resolveNotifyHeadline(eventType, message); + String detail = normalizeNotifyDetailLines(eventType, message); StringBuilder sb = new StringBuilder(); - sb.append("【闲鱼订单】").append(orderNo != null ? orderNo : "-").append("\n"); - String typeLabel = humanEventType(eventType); - String srcLabel = humanSource(source); - if (StringUtils.hasText(typeLabel)) { - sb.append(typeLabel); - if (StringUtils.hasText(srcLabel)) { - sb.append(" · ").append(srcLabel); - } - } else if (StringUtils.hasText(srcLabel)) { - sb.append(srcLabel); - } - sb.append("\n"); - if (StringUtils.hasText(message)) { - sb.append(message); + sb.append(head).append("\n订单号:").append(on).append('\n'); + if (StringUtils.hasText(detail)) { + sb.append(detail); } String s = sb.toString(); if (s.length() > CONTENT_MAX) { @@ -134,48 +129,221 @@ public class WxSendGoofishNotifyClient { return s; } - /** 企微展示用,与库内 event_type 枚举一致 */ - private static String humanEventType(String eventType) { - if (!StringUtils.hasText(eventType)) { - return ""; + private static String resolveNotifyHeadline(String eventType, String message) { + String m = message != null ? message : ""; + String t = eventType != null ? eventType.trim() : ""; + if ("SHIP".equals(t)) { + if (m.contains("失败") || m.contains("异常") || m.contains("缺地址")) { + return "发货失败"; + } + return "发货"; } - switch (eventType.trim()) { - case "ORDER_SYNC": - return "订单同步"; - case "LOGISTICS_SYNC": - return "物流同步"; - case "SHIP": - return "发货"; - default: - return eventType.trim(); + if ("LOGISTICS_SYNC".equals(t)) { + return "物流"; } + if ("ORDER_SYNC".equals(t)) { + if (m.startsWith("新订单") || m.contains("新订单入库")) { + return "新订单"; + } + if (looksRefundOnlyDiffLine(m)) { + return "退款"; + } + return "订单"; + } + return StringUtils.hasText(t) ? t : "订单"; } - /** 企微展示用,与库内 source 一致 */ - private static String humanSource(String source) { - if (!StringUtils.hasText(source)) { + /** ORDER_SYNC 正文里是否仅有退款状态变更(单行或多行中的唯一语义块) */ + private static boolean looksRefundOnlyDiffLine(String message) { + if (!StringUtils.hasText(message) || message.contains("新订单")) { + return false; + } + String normalized = message.replace(';', '\n').trim(); + String[] lines = normalized.split("\n"); + boolean sawRefund = false; + boolean sawOrder = false; + for (String raw : lines) { + String line = raw.trim(); + if (line.isEmpty()) { + continue; + } + if (line.startsWith("退款状态")) { + sawRefund = true; + continue; + } + if (line.startsWith("订单状态")) { + sawOrder = true; + continue; + } + return false; + } + return sawRefund && !sawOrder; + } + + /** + * 将内部摘要句转为「字段:值」行;箭头统一为「 → 」。 + */ + private static String normalizeNotifyDetailLines(String eventType, String message) { + if (!StringUtils.hasText(message)) { return ""; } - switch (source.trim()) { - case "LIST_UPSERT": - return "列表拉单写入"; - case "NOTIFY_UPSERT": - return "推送回调写入"; - case "LIST": - return "列表摘要"; - case "DETAIL_REFRESH": - return "详情刷新"; - case "NOTIFY": - return "推送"; - case "REDIS_WAYBILL": - return "Redis 运单"; - case "AUTO_SHIP": - return "自动发货"; - case "JD_LOGISTICS_PUSH": - return "京东物流扫描"; - default: - return source.trim(); + String t = eventType != null ? eventType.trim() : ""; + String m = message.trim(); + + if ("SHIP".equals(t)) { + if (m.startsWith("发货成功")) { + StringBuilder details = new StringBuilder(); + String wb = extractCommaField(m, "运单 "); + if (StringUtils.hasText(wb)) { + details.append("物流单号:").append(normalizeArrowSpaces(wb)).append('\n'); + } + String os = extractCommaField(m, "订单状态 "); + if (StringUtils.hasText(os)) { + details.append("订单状态:").append(normalizeArrowSpaces(os)).append('\n'); + } + String rs = extractCommaField(m, "退款状态 "); + if (StringUtils.hasText(rs)) { + details.append("退款状态:").append(normalizeArrowSpaces(rs)).append('\n'); + } + if (details.length() > 0) { + return details.toString(); + } + } + if (m.startsWith("发货失败") || m.startsWith("发货异常")) { + int idx = m.indexOf(':'); + String reason = idx >= 0 && idx < m.length() - 1 ? m.substring(idx + 1).trim() : m; + return "原因:" + normalizeArrowSpaces(reason) + "\n"; + } } + + if ("ORDER_SYNC".equals(t)) { + // 新订单入库,订单状态 X,退款状态 Y + if (m.contains("新订单入库") && m.contains("订单状态 ") && m.contains("退款状态 ")) { + String os = substringBetweenPrefixes(m, "订单状态 ", ",退款状态"); + String rs = substringAfterPrefix(m, "退款状态 "); + StringBuilder sb = new StringBuilder(); + if (StringUtils.hasText(os)) { + sb.append("订单状态:").append(os.trim()).append('\n'); + } + if (StringUtils.hasText(rs)) { + sb.append("退款状态:").append(rs.trim()).append('\n'); + } + return sb.toString(); + } + // 订单状态 A → B;退款状态 …(仅输出消息里出现的块) + return formatOrderSyncSemicolonParts(m); + } + + if ("LOGISTICS_SYNC".equals(t)) { + return formatLogisticsParts(m); + } + + return normalizeArrowSpaces(m) + "\n"; + } + + private static String formatOrderSyncSemicolonParts(String m) { + String[] parts = m.split(";"); + StringBuilder sb = new StringBuilder(); + for (String p : parts) { + String line = kvLineOrderOrRefund(p.trim()); + if (line != null) { + sb.append(line).append('\n'); + } + } + return sb.toString(); + } + + private static String kvLineOrderOrRefund(String segment) { + if (!StringUtils.hasText(segment)) { + return null; + } + if (segment.startsWith("订单状态 ")) { + return "订单状态:" + normalizeArrowSpaces(segment.substring("订单状态 ".length()).trim()); + } + if (segment.startsWith("退款状态 ")) { + return "退款状态:" + normalizeArrowSpaces(segment.substring("退款状态 ".length()).trim()); + } + return null; + } + + private static String formatLogisticsParts(String m) { + String[] parts = m.split(";"); + StringBuilder sb = new StringBuilder(); + for (String p : parts) { + String line = logisticsSegmentToKv(p.trim()); + if (line != null) { + sb.append(line).append('\n'); + } + } + return sb.toString(); + } + + /** 本地/平台运单 → 对用户展示为物流单号;其它变更行仍可保留技术性标签 */ + private static String logisticsSegmentToKv(String segment) { + if (!StringUtils.hasText(segment)) { + return null; + } + if (segment.startsWith("本地运单 ") || segment.startsWith("平台运单 ")) { + int cut = segment.indexOf(' '); + String rest = normalizeArrowSpaces(segment.substring(cut + 1).trim()); + return "物流单号:" + rest; + } + String kv = logisticLabelToColon(segment); + return kv != null ? kv : (normalizeArrowSpaces(segment)); + } + + private static String logisticLabelToColon(String segment) { + String[] prefixes = {"快递编码 ", "快递名称 "}; + for (String pref : prefixes) { + if (segment.startsWith(pref)) { + String label = pref.trim(); + return label.substring(0, label.length() - 1) + ":" + + normalizeArrowSpaces(segment.substring(pref.length()).trim()); + } + } + return null; + } + + private static String normalizeArrowSpaces(String s) { + if (s == null) { + return ""; + } + return s.replace("→", " → ").replace(" →", " →").replace("→ ", " → "); + } + + /** 从「片段1,片段2,…」中取出以 {@code fieldPrefix} 开头的片段值(去掉前缀) */ + private static String extractCommaField(String m, String fieldPrefix) { + if (!StringUtils.hasText(m) || !StringUtils.hasText(fieldPrefix)) { + return null; + } + for (String part : m.split(",")) { + String p = part.trim(); + if (p.startsWith(fieldPrefix)) { + String v = p.substring(fieldPrefix.length()).trim(); + return v.length() > 0 ? v : null; + } + } + return null; + } + + private static String substringBetweenPrefixes(String m, String a, String b) { + int ia = m.indexOf(a); + if (ia < 0) { + return null; + } + int ib = m.indexOf(b, ia + a.length()); + if (ib < 0) { + return null; + } + return m.substring(ia + a.length(), ib); + } + + private static String substringAfterPrefix(String m, String pref) { + int i = m.indexOf(pref); + if (i < 0) { + return null; + } + return m.substring(i + pref.length()).trim(); } /**