This commit is contained in:
van
2026-05-06 17:38:58 +08:00
parent 9d03cca517
commit 7582868b2c
4 changed files with 303 additions and 62 deletions

View File

@@ -33,6 +33,11 @@ public class GoofishOrderChangeLogger {
*/ */
public static final String SOURCE_JD_LOGISTICS_PUSH = "JD_LOGISTICS_PUSH"; public static final String SOURCE_JD_LOGISTICS_PUSH = "JD_LOGISTICS_PUSH";
/**
* Redis 写入本地运单:与后续详情运单合并、发货成功通知重复度高,仅落库便于对账,不推企微。
*/
public static final String SOURCE_REDIS_WAYBILL = "REDIS_WAYBILL";
@Resource @Resource
private ErpGoofishOrderEventLogMapper erpGoofishOrderEventLogMapper; private ErpGoofishOrderEventLogMapper erpGoofishOrderEventLogMapper;
@@ -40,6 +45,14 @@ public class GoofishOrderChangeLogger {
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient; private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message) { 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)) { if (orderId == null || StringUtils.isEmpty(message)) {
return; return;
} }
@@ -59,7 +72,10 @@ public class GoofishOrderChangeLogger {
return; return;
} }
log.info("[goofish-order-event] orderId={} orderNo={} type={} source={} {}", orderId, orderNo, eventType, source, msg); 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; return;
} }
try { try {
@@ -82,16 +98,21 @@ public class GoofishOrderChangeLogger {
RowSnap b = RowSnap.from(beforeSnap); RowSnap b = RowSnap.from(beforeSnap);
RowSnap a = RowSnap.from(afterRow); RowSnap a = RowSnap.from(afterRow);
boolean refundDiff = !Objects.equals(b.refundStatus, a.refundStatus);
boolean orderDiff = !Objects.equals(b.orderStatus, a.orderStatus);
List<String> orderParts = new ArrayList<>(); List<String> orderParts = new ArrayList<>();
if (!Objects.equals(b.orderStatus, a.orderStatus)) { if (orderDiff) {
orderParts.add("订单状态 " + GoofishStatusLabels.orderStatusChange(b.orderStatus, a.orderStatus)); 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)); orderParts.add("退款状态 " + GoofishStatusLabels.refundStatusChange(b.refundStatus, a.refundStatus));
} }
if (!orderParts.isEmpty()) { if (!orderParts.isEmpty()) {
boolean notifyWecom = refundDiff || (orderDiff
&& GoofishStatusLabels.isWxNotifiableOrderStatusChange(b.orderStatus, a.orderStatus));
append(afterRow.getId(), afterRow.getAppKey(), afterRow.getOrderNo(), TYPE_ORDER, source, append(afterRow.getId(), afterRow.getAppKey(), afterRow.getOrderNo(), TYPE_ORDER, source,
String.join("", orderParts)); String.join("", orderParts), notifyWecom);
} }
List<String> logParts = new ArrayList<>(); List<String> logParts = new ArrayList<>();

View File

@@ -143,17 +143,27 @@ public class GoofishOrderPipeline {
if (existingBeforeUpdate == null) { if (existingBeforeUpdate == null) {
goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(), goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(),
GoofishOrderChangeLogger.TYPE_ORDER, upsertSource, GoofishOrderChangeLogger.TYPE_ORDER, upsertSource,
"新订单入库,订单状态 " + GoofishStatusLabels.orderStatusHuman(loaded.getOrderStatus()) "新订单入库,订单状态 " + GoofishStatusLabels.orderStatusHumanForNotify(loaded.getOrderStatus())
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(loaded.getRefundStatus())); + ",退款状态 " + GoofishStatusLabels.refundStatusHuman(loaded.getRefundStatus()));
return; return;
} }
if (!Objects.equals(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus()) boolean refundDiff = !Objects.equals(existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus());
|| !Objects.equals(existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus())) { boolean orderDiff = !Objects.equals(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus());
if (orderDiff || refundDiff) {
List<String> 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.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(),
GoofishOrderChangeLogger.TYPE_ORDER, upsertSource, GoofishOrderChangeLogger.TYPE_ORDER, upsertSource,
"订单状态 " + GoofishStatusLabels.orderStatusChange(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus()) String.join("", parts), notifyWecom);
+ ";退款状态 " + GoofishStatusLabels.refundStatusChange(
existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus()));
} }
} }
@@ -425,7 +435,7 @@ public class GoofishOrderPipeline {
row.setLocalWaybillNo(wb.trim()); row.setLocalWaybillNo(wb.trim());
if (goofishOrderChangeLogger != null) { if (goofishOrderChangeLogger != null) {
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(), 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()); "本地运单 " + (prev == null || prev.isEmpty() ? "" : prev) + "" + wb.trim());
} }
} }
@@ -532,7 +542,9 @@ public class GoofishOrderPipeline {
if (goofishOrderChangeLogger != null) { if (goofishOrderChangeLogger != null) {
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(), goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP", GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
"发货成功,运单 " + waybill.trim() + ",快递编码 " + expressCode); "发货成功,运单 " + waybill.trim()
+ ",订单状态 " + GoofishStatusLabels.orderStatusHuman(row.getOrderStatus())
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(row.getRefundStatus()));
} }
} else { } else {
String msg = r != null ? r.getString("msg") : "unknown"; String msg = r != null ? r.getString("msg") : "unknown";

View File

@@ -2,6 +2,7 @@ package com.ruoyi.jarvis.service.goofish;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
/** /**
* 闲管家开放平台:订单状态、退款状态中文说明(与 Apifox 订单列表 schema 一致) * 闲管家开放平台:订单状态、退款状态中文说明(与 Apifox 订单列表 schema 一致)
@@ -79,6 +80,45 @@ public final class GoofishStatusLabels {
return orderStatusHuman(from) + "" + orderStatusHuman(to); 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) { public static String refundStatusChange(Integer from, Integer to) {
return refundStatusHuman(from) + "" + refundStatusHuman(to); return refundStatusHuman(from) + "" + refundStatusHuman(to);
} }

View File

@@ -110,22 +110,17 @@ public class WxSendGoofishNotifyClient {
} }
} }
/**
* 企微短通知:场景标题 + 订单号 + 必要字段行(不再展示数据来源等冗长前缀)。
*/
private static String buildContent(String orderNo, String eventType, String source, String message) { 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(); StringBuilder sb = new StringBuilder();
sb.append("【闲鱼订单】").append(orderNo != null ? orderNo : "-").append("\n"); sb.append(head).append("\n订单号").append(on).append('\n');
String typeLabel = humanEventType(eventType); if (StringUtils.hasText(detail)) {
String srcLabel = humanSource(source); sb.append(detail);
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);
} }
String s = sb.toString(); String s = sb.toString();
if (s.length() > CONTENT_MAX) { if (s.length() > CONTENT_MAX) {
@@ -134,48 +129,221 @@ public class WxSendGoofishNotifyClient {
return s; return s;
} }
/** 企微展示用,与库内 event_type 枚举一致 */ private static String resolveNotifyHeadline(String eventType, String message) {
private static String humanEventType(String eventType) { String m = message != null ? message : "";
if (!StringUtils.hasText(eventType)) { String t = eventType != null ? eventType.trim() : "";
return ""; if ("SHIP".equals(t)) {
if (m.contains("失败") || m.contains("异常") || m.contains("缺地址")) {
return "发货失败";
}
return "发货";
} }
switch (eventType.trim()) { if ("LOGISTICS_SYNC".equals(t)) {
case "ORDER_SYNC": return "物流";
return "订单同步";
case "LOGISTICS_SYNC":
return "物流同步";
case "SHIP":
return "发货";
default:
return eventType.trim();
} }
if ("ORDER_SYNC".equals(t)) {
if (m.startsWith("新订单") || m.contains("新订单入库")) {
return "新订单";
}
if (looksRefundOnlyDiffLine(m)) {
return "退款";
}
return "订单";
}
return StringUtils.hasText(t) ? t : "订单";
} }
/** 企微展示用,与库内 source 一致 */ /** ORDER_SYNC 正文里是否仅有退款状态变更(单行或多行中的唯一语义块) */
private static String humanSource(String source) { private static boolean looksRefundOnlyDiffLine(String message) {
if (!StringUtils.hasText(source)) { 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 ""; return "";
} }
switch (source.trim()) { String t = eventType != null ? eventType.trim() : "";
case "LIST_UPSERT": String m = message.trim();
return "列表拉单写入";
case "NOTIFY_UPSERT": if ("SHIP".equals(t)) {
return "推送回调写入"; if (m.startsWith("发货成功")) {
case "LIST": StringBuilder details = new StringBuilder();
return "列表摘要"; String wb = extractCommaField(m, "运单 ");
case "DETAIL_REFRESH": if (StringUtils.hasText(wb)) {
return "详情刷新"; details.append("物流单号:").append(normalizeArrowSpaces(wb)).append('\n');
case "NOTIFY": }
return "推送"; String os = extractCommaField(m, "订单状态 ");
case "REDIS_WAYBILL": if (StringUtils.hasText(os)) {
return "Redis 运单"; details.append("订单状态:").append(normalizeArrowSpaces(os)).append('\n');
case "AUTO_SHIP": }
return "自动发货"; String rs = extractCommaField(m, "退款状态 ");
case "JD_LOGISTICS_PUSH": if (StringUtils.hasText(rs)) {
return "京东物流扫描"; details.append("退款状态:").append(normalizeArrowSpaces(rs)).append('\n');
default: }
return source.trim(); 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();
} }
/** /**