diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/OrderProfitAnalysisServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/OrderProfitAnalysisServiceImpl.java
index 3c5d553..4626cc6 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/OrderProfitAnalysisServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/OrderProfitAnalysisServiceImpl.java
@@ -35,6 +35,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
/**
* 慢单利润/订单 AI 分析。数据来自 jd_order,提示词走 Redis social_media:prompt:order:profit_analysis,
@@ -50,6 +51,9 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
private static final String REDIS_LATEST_PREFIX = "order_profit_analysis:latest:";
private static final String REDIS_SEND_LOG = "order_profit_analysis:send_log";
private static final int SEND_LOG_MAX = 80;
+ private static final Pattern THINK_BLOCK = Pattern.compile(
+ "(?is)]*>.*?");
+ private static final Pattern THINK_TAG = Pattern.compile("(?i)?think\\b[^>]*>");
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -58,15 +62,44 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
private static final int WECOM_CHUNK_BYTES = 1800;
public static final String DEFAULT_PROMPT =
- "你是电商分销订单与利润分析助手。下面是已经汇总好的 JSON 统计(慢单 jd_order;"
- + "仅 is_count_enabled=1 参与;已退款单不计入付款/后返/利润)。\n\n"
- + "请基于数据写一份简洁中文报告,不要编造 JSON 里没有的数字。结构如下:\n"
+ "你是电商分销订单与利润分析助手。下面 JSON 已按慢单 jd_order 汇总,不要编造其中没有的数字。\n\n"
+ + "# 统计口径(必须遵守)\n"
+ + "- 只含 is_count_enabled=1 的单;已退款单计入 orderCount/refundCount,但不计入付款、后返、成本、售价、利润。\n"
+ + "- 金额单位均为元,保留两位小数。\n"
+ + "- 日报对比「昨天」、周报对比「上一完整周」、月报对比「上一自然月」。\n\n"
+ + "# JSON 字段说明\n"
+ + "periodType:day=日报 / week=周报 / month=月报。\n"
+ + "periodLabel:给人看的周期标题。\n"
+ + "timezone:统计时区。\n"
+ + "current / previous:本期、上期汇总,结构相同。\n"
+ + "current.beginDate / endDate:本期起止日期(含当天)。\n"
+ + "current.label:本期短标题。\n"
+ + "current.orderCount:参与统计的总单量(含退款)。\n"
+ + "current.refundCount:其中已退款单量。\n"
+ + "current.validOrderCount:有效单量 = orderCount - refundCount。\n"
+ + "current.missingProfitCount:有效单里 profit 为空的数量(还没定价/没算利润)。\n"
+ + "current.totalPayment:有效单下单付款合计(顾客付给京东的钱)。\n"
+ + "current.totalRebate:有效单后返合计(平台/团长返给分销的钱)。\n"
+ + "current.totalCost:进货成本合计 = totalPayment - totalRebate。\n"
+ + "current.totalSellingPrice:对客售价合计(直款或闲鱼成交价,未扣闲鱼手续费)。\n"
+ + "current.totalExtraCost:额外成本合计(运费补贴、补差等,已从利润里扣)。\n"
+ + "current.totalProfit:有效单利润合计(空利润的单不计入,不是按售价现场重算)。\n"
+ + "current.avgProfit:单均利润 = totalProfit / validOrderCount(分母含缺利润单,会拉低均值)。\n"
+ + "current.profitRatePct:利润率% = totalProfit / totalPayment × 100。\n"
+ + "current.byMark:按分销标记分组,按利润降序。F / F-xxx=发单,PDD=拼多多,H-TF=固定利润约 15。\n"
+ + "current.byModel:按型号(型号本体+店铺前缀)分组,只保留利润前 15。\n"
+ + "current.byBuyer:按下单人前缀分组(第一个「-」前视为同一人),只保留利润前 15。\n"
+ + "分组对象字段:name=分组名,orderCount=有效单量,payment=付款,rebate=后返,extraCost=额外成本,profit=利润。\n"
+ + "compare:本期相对上期。每个指标含 current/previous/delta/changePct。\n"
+ + "compare.*.delta:本期减上期;正数=增加,负数=减少。\n"
+ + "compare.*.changePct:环比幅度%,上期为 0 且本期>0 时记为 100。\n\n"
+ + "# 写作要求\n"
+ "1. 周期结论(1-2 句)\n"
- + "2. 订单与利润对比:本期 vs 上期,写明增减和幅度\n"
+ + "2. 订单与利润对比:用 compare 写明增减和幅度,点明利润率、单均利润\n"
+ "3. 结构拆解:分销标记 / 型号 / 下单人前缀的亮点与异常\n"
- + "4. 风险:退款、利润缺失、额外成本\n"
- + "5. 建议:1-3 条可执行动作\n\n"
- + "输出纯文本,适合企业微信阅读,控制在 800 字以内,可用简单条目,不要用 Markdown 代码块。\n\n"
+ + "4. 风险:退款、利润缺失、额外成本偏高\n"
+ + "5. 建议:1-3 条可执行动作\n"
+ + "输出纯文本,适合企业微信,控制在 800 字以内,可用简单条目,不要用 Markdown 代码块。\n\n"
+ "统计数据:\n{{stats_json}}";
@Autowired
@@ -169,6 +202,10 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
if (StringUtils.isEmpty(report)) {
throw new RuntimeException("大模型返回为空");
}
+ report = stripThinkTags(report);
+ if (StringUtils.isEmpty(report)) {
+ throw new RuntimeException("大模型仅返回思考过程,无正文");
+ }
} catch (Exception e) {
aiError = e.getMessage();
log.warn("慢单利润 AI 分析失败,改用数字摘要: {}", aiError);
@@ -395,11 +432,37 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
log.warn("利润分析推送跳过:未配置 jarvis.order-profit-analysis.touser 或 jarvis.wecom.goofish-notify-touser");
return false;
}
- String full = (title == null ? "" : title) + "\n" + (report == null ? "" : report.trim());
+ String body = stripThinkTags(report);
+ String full = (title == null ? "" : title) + "\n" + (body == null ? "" : body);
List chunks = splitUtf8Chunks(full, WECOM_CHUNK_BYTES);
return phoneForwardActivePush.schedulePushChunks(toUser, null, chunks);
}
+ /**
+ * 去掉模型思考标签及其内容,避免推到企微。
+ * 兼容成对 {@code ...},以及残留的开闭标签。
+ */
+ static String stripThinkTags(String text) {
+ if (StringUtils.isEmpty(text)) {
+ return "";
+ }
+ String s = THINK_BLOCK.matcher(text).replaceAll("");
+ int orphanEnd = indexOfIgnoreCase(s, "");
+ if (orphanEnd >= 0) {
+ s = s.substring(orphanEnd + "".length());
+ }
+ s = THINK_TAG.matcher(s).replaceAll("");
+ s = s.replaceAll("[ \\t]+\\n", "\n").replaceAll("\\n{3,}", "\n\n").trim();
+ return s;
+ }
+
+ private static int indexOfIgnoreCase(String text, String needle) {
+ if (text == null || needle == null) {
+ return -1;
+ }
+ return text.toLowerCase().indexOf(needle.toLowerCase());
+ }
+
private String resolveTouser() {
return normalizeTouser(StringUtils.isNotEmpty(analysisTouser) ? analysisTouser : fallbackTouser);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java
index 934aa76..9ae405e 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java
@@ -72,7 +72,7 @@ public class SocialMediaServiceImpl implements ISocialMediaService
put("xianyu:jiaonixiadan_extra", "闲鱼文案·教你下单版尾部附加说明\n接在「更新日期:yyyy-MM-dd」之后(纯文本)");
put("xianyu:title_clean_regex", "闲鱼文案·标题/型号清洗正则\n从标题与型号备注中移除营销敏感片段;须为 Java 正则,匹配到的内容会被删除");
put("xianyu:seed_note_prompt", "闲鱼文案·种草文案提示词模板\n占位符:{{goods_title}} {{goods_model}} {{goods_type}} {{goods_brand}} {{channel_source}} {{official_price}} {{sell_price}} {{warranty}} {{delivery_install}}");
- put("order:profit_analysis", "慢单利润/订单 AI 分析提示词\n占位符:{{stats_json}} — 本期/上期/对比统计 JSON");
+ put("order:profit_analysis", "慢单利润/订单 AI 分析提示词\n内置含 JSON 字段口径说明;占位符:{{stats_json}}");
}};
/** 多套大模型 JSON 存储,与 Jarvis_java SocialMediaLlmClient 一致 */