From e8e1f9e26fe12bf7b8bfef25c62c65d5679bff7f Mon Sep 17 00:00:00 2001 From: van Date: Wed, 30 Sep 2026 17:33:05 +0800 Subject: [PATCH] 1 --- .../impl/OrderProfitAnalysisServiceImpl.java | 443 ++++++++++++++++-- 1 file changed, 402 insertions(+), 41 deletions(-) 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 17a4c89..c9d7589 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 @@ -2,7 +2,6 @@ package com.ruoyi.jarvis.service.impl; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; -import com.alibaba.fastjson2.JSONWriter; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.jarvis.domain.JDOrder; import com.ruoyi.jarvis.service.IJDOrderService; @@ -28,6 +27,7 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; @@ -63,8 +63,16 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi public static final String DEFAULT_PROMPT = "你是电商分销订单与利润分析助手。下面 JSON 已按慢单 jd_order 汇总,不要编造其中没有的数字。\n\n" + + "# 分销口径(必须先读,决定利润怎么算)\n" + + "- F 开头 = 发单。F,以及带连字符的 F-xxx,除去 F-W、F-H = 完全自有单。利润来自售价减去成本和额外成本。\n" + + "- 仅 F-W、F-H,或以 F-W-、F-H- 再带后缀 = 代下单。利润来源只有代下费用。F-Hello 这类不是 F-H。\n" + + "- 非 F 开头(含 H-TF、TF、PDD、H- 等所有其余前缀)= 代下单单。利润只来自代下佣金,单均利润天然低,属正常。\n" + + "- 未标记不要并进上面三类。\n" + + "- 自有单、代下单、代下单单必须分开写:各几单、各多少利润、各多少利润率,不允许合并。\n" + + "- 这三类的数字直接读 kind 和 kd,不要自己把 byMark 加总。\n" + + "- JSON 里的利润是订单上已经算好的 profit,不要按售价现场重算。\n\n" + "# 统计口径(必须遵守)\n" - + "- 只含 is_count_enabled=1 的单;已退款单计入 orderCount/refundCount,但不计入付款、后返、成本、售价、利润。\n" + + "- 只含 is_count_enabled=1 的单;已退款单计入总单量和退款单量,但不计入付款、后返、成本、售价、利润。\n" + "- 金额单位均为元,保留两位小数。\n" + "- 日报对比「昨天」、周报对比「上一完整周」、月报对比「上一自然月」。\n\n" + "# 资金与后返(必须遵守,禁止说反)\n" @@ -73,37 +81,29 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi + "- 后返越少越好:同样利润下,后返少 = 占用资金少、垫付少。0 后返且利润为正,是资金效率更好,不是缺陷。\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" + + "# JSON 结构(数组列,时区固定 Asia/Shanghai)\n" + + "不要把订单逐条展开。列名在 JSON 顶部,不要把 _n/_amt/_row/_kind/_d/_kd 当成统计数字。\n" + + "period:day=日报 / week=周报 / month=月报。label:周期标题。\n" + + "cur / prev:本期、上期,结构相同。range:[开始日期, 结束日期](含当天)。\n" + + "n 按 _n = [总单量(含退款), 退款单量, 有效单量, 缺利润单量]。有效 = 总单 - 退款。缺利润 = 有效单里 profit 为空。\n" + + "amt 按 _amt = [付款, 后返, 成本, 售价, 额外成本, 利润, 单均利润, 利润率%]。\n" + + "付款是垫给京东的钱。后返是垫资里稍后收回的部分,越少越好,不是业绩。成本 = 付款 - 后返。\n" + + "售价是对客价(未扣闲鱼手续费)。额外成本含运费补贴、补差,已从利润里扣。\n" + + "利润是有效单利润合计。单均利润 = 利润 / 有效单量(分母含缺利润单)。利润率% = 利润 / 付款 × 100。\n" + + "kind:分类型汇总,每行按 _kind。类型为 自有单、代下单、代下单单;有未标记时多一行。\n" + + "byMark / byModel / byBuyer:每行按 _row = [名称, 有效单量, 付款, 后返, 额外成本, 利润, 类型],已按利润降序。\n" + + "byMark 为全部分销标记。byModel 为型号(型号本体+店铺前缀),byBuyer 为下单人前缀(第一个「-」前)。二者只留利润前 15,同一名称按类型拆成多行。\n" + + "d:整体对比。键为 总单/退款/付款/后返/利润/单均/利润率,值按 _d = [增减, 环比%]。\n" + + "kd:三类分开对比,每行按 _kd。利润率百分点 = 本期利润率减上期利润率,不是环比百分比。\n" + + "增减 = 本期减上期;正数=增加,负数=减少。环比% 在上期为 0 且本期>0 时记为 100。\n\n" + "# 写作要求\n" - + "1. 周期结论(1-2 句)\n" - + "2. 订单与利润对比:用 compare 写明增减和幅度,点明利润率、单均利润\n" - + "3. 结构拆解:分销标记 / 型号 / 下单人前缀的亮点与异常\n" - + "4. 风险:退款、利润缺失、额外成本偏高、利润率偏低;不要把后返低写成风险\n" + + "1. 周期结论(1-2句):用 kind 分别点出自有单、代下单、代下单单的有效单量、利润、利润率\n" + + "2. 订单与利润对比:整体用 d;三类用 kd 分开写增减和幅度,点明利润率、单均利润\n" + + "3. 结构拆解:\n" + + " - 分销标记:以 kind 为准写自有单、代下单、代下单单,不要合并\n" + + " - 型号:看 byModel 前 15 里各类型占多少\n" + + " - 下单人前缀:看 byBuyer 的类型列,分清自有单前缀和代下单单前缀,代下单单重点看佣金(利润)\n" + + "4. 风险:退款、利润缺失、额外成本偏高、利润率偏低;不要把后返低写成风险。代下单单利润率低属正常,不要单独当成异常\n" + "5. 建议:1-3 条可执行动作(不要建议「提高后返」)\n" + "输出纯文本,适合企业微信,控制在 800 字以内,可用简单条目,不要用 Markdown 代码块。\n\n" + "统计数据:\n{{stats_json}}"; @@ -136,7 +136,9 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi @Override public Map buildStats(String periodType, String date) { PeriodRange current = resolvePeriod(periodType, date, false); - return assemblePayload(periodType, current, false); + Map stats = assemblePayload(periodType, current, false); + attachLlmJson(stats); + return stats; } @Override @@ -202,8 +204,10 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi private Map doAnalyze(String periodType, PeriodRange current, boolean push, String profileId, String trigger) { Map stats = assemblePayload(periodType, current, true); - String statsJson = JSON.toJSONString(stats, JSONWriter.Feature.PrettyFormat); - String prompt = loadPrompt().replace("{{stats_json}}", statsJson); + String statsJson = toLlmJson(stats); + String prompt = renderPrompt(statsJson); + log.info("慢单利润分析提交 period={} llmJsonChars={} promptChars={}", + current.label, statsJson.length(), prompt.length()); String report; String aiError = null; @@ -248,6 +252,8 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi result.put("generatedAt", java.time.LocalDateTime.now(ZONE).format(TS)); result.put("trigger", normalizeTrigger(trigger)); result.put("stats", stats); + result.put("llmJson", statsJson); + result.put("llmJsonChars", statsJson.length()); result.put("report", report); result.put("aiError", aiError); result.put("pushed", pushed); @@ -273,12 +279,157 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi payload.put("current", cur); payload.put("previous", prev); payload.put("compare", compare(cur, prev)); + payload.put("kindCompare", compareKinds(cur, prev)); if (!forLlm) { payload.put("generatedAt", java.time.LocalDateTime.now(ZONE).format(TS)); } return payload; } + private static void attachLlmJson(Map stats) { + String llmJson = toLlmJson(stats); + stats.put("llmJson", llmJson); + stats.put("llmJsonChars", llmJson.length()); + } + + /** + * 交给模型的紧凑统计。页面卡片仍用对象;这个字符串才是实际提交,分组和指标都是列数组。 + */ + static String toLlmJson(Map stats) { + Map out = new LinkedHashMap<>(); + out.put("_n", Arrays.asList("总单", "退款", "有效", "缺利润")); + out.put("_amt", Arrays.asList("付款", "后返", "成本", "售价", "额外成本", "利润", "单均", "利润率%")); + out.put("_kind", Arrays.asList("类型", "总单", "退款", "有效", "缺利润", "付款", "后返", "成本", "售价", "额外成本", "利润", "单均", "利润率%")); + out.put("_row", Arrays.asList("名称", "单量", "付款", "后返", "额外成本", "利润", "类型")); + out.put("_d", Arrays.asList("增减", "环比%")); + out.put("_kd", Arrays.asList("类型", "有效增减", "有效环比%", "利润增减", "利润环比%", "利润率百分点")); + out.put("period", stats.get("periodType")); + out.put("label", stats.get("periodLabel")); + out.put("cur", compactPeriod(castMap(stats.get("current")))); + out.put("prev", compactPeriod(castMap(stats.get("previous")))); + out.put("d", compactDelta(castMap(stats.get("compare")))); + out.put("kd", compactKindDelta(stats.get("kindCompare"))); + return JSON.toJSONString(out); + } + + private static Map compactPeriod(Map period) { + Map out = new LinkedHashMap<>(); + if (period == null) { + out.put("range", Arrays.asList("", "")); + out.put("n", Arrays.asList(0, 0, 0, 0)); + out.put("amt", Arrays.asList(0, 0, 0, 0, 0, 0, 0, 0)); + out.put("kind", Collections.emptyList()); + out.put("byMark", Collections.emptyList()); + out.put("byModel", Collections.emptyList()); + out.put("byBuyer", Collections.emptyList()); + return out; + } + out.put("range", Arrays.asList(period.get("beginDate"), period.get("endDate"))); + out.put("n", Arrays.asList( + period.get("orderCount"), period.get("refundCount"), + period.get("validOrderCount"), period.get("missingProfitCount"))); + out.put("amt", Arrays.asList( + period.get("totalPayment"), period.get("totalRebate"), period.get("totalCost"), + period.get("totalSellingPrice"), period.get("totalExtraCost"), period.get("totalProfit"), + period.get("avgProfit"), period.get("profitRatePct"))); + out.put("kind", compactKindRows(period.get("byKind"))); + out.put("byMark", compactRows(period.get("byMark"))); + out.put("byModel", compactRows(firstList(period.get("byModelSplit"), period.get("byModel")))); + out.put("byBuyer", compactRows(firstList(period.get("byBuyerSplit"), period.get("byBuyer")))); + return out; + } + + private static List> compactRows(Object groups) { + List> rows = new ArrayList<>(); + if (!(groups instanceof List)) { + return rows; + } + for (Object item : (List) groups) { + Map row = castMap(item); + if (row == null) { + continue; + } + rows.add(Arrays.asList( + row.get("name"), row.get("orderCount"), row.get("payment"), + row.get("rebate"), row.get("extraCost"), row.get("profit"), + row.get("kind") == null ? "" : row.get("kind"))); + } + return rows; + } + + private static Map compactDelta(Map compare) { + Map out = new LinkedHashMap<>(); + String[][] keys = { + {"orderCount", "总单"}, + {"refundCount", "退款"}, + {"totalPayment", "付款"}, + {"totalRebate", "后返"}, + {"totalProfit", "利润"}, + {"avgProfit", "单均"}, + {"profitRatePct", "利润率"} + }; + for (String[] key : keys) { + Map item = compare == null ? null : castMap(compare.get(key[0])); + if (item == null) { + out.put(key[1], Arrays.asList(0, 0)); + continue; + } + out.put(key[1], Arrays.asList(item.get("delta"), item.get("changePct"))); + } + return out; + } + + private static List> compactKindRows(Object groups) { + List> rows = new ArrayList<>(); + if (!(groups instanceof List)) { + return rows; + } + for (Object item : (List) groups) { + Map row = castMap(item); + if (row == null) { + continue; + } + rows.add(Arrays.asList( + row.get("name"), row.get("orderCount"), row.get("refundCount"), row.get("validOrderCount"), + row.get("missingProfitCount"), row.get("totalPayment"), row.get("totalRebate"), row.get("totalCost"), + row.get("totalSellingPrice"), row.get("totalExtraCost"), row.get("totalProfit"), + row.get("avgProfit"), row.get("profitRatePct"))); + } + return rows; + } + + private static List> compactKindDelta(Object groups) { + List> rows = new ArrayList<>(); + if (!(groups instanceof List)) { + return rows; + } + for (Object item : (List) groups) { + Map row = castMap(item); + if (row == null) { + continue; + } + rows.add(Arrays.asList( + row.get("name"), row.get("validDelta"), row.get("validChangePct"), + row.get("profitDelta"), row.get("profitChangePct"), row.get("rateDelta"))); + } + return rows; + } + + private static Object firstList(Object preferred, Object fallback) { + if (preferred instanceof List && !((List) preferred).isEmpty()) { + return preferred; + } + return fallback; + } + + @SuppressWarnings("unchecked") + private static Map castMap(Object value) { + if (value instanceof Map) { + return (Map) value; + } + return null; + } + private Map summarize(PeriodRange range) { JDOrder query = new JDOrder(); query.getParams().put("beginTime", range.begin.format(DAY)); @@ -299,15 +450,23 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi Map byMark = new LinkedHashMap<>(); Map byModel = new LinkedHashMap<>(); Map byBuyer = new LinkedHashMap<>(); + Map byModelSplit = new LinkedHashMap<>(); + Map byBuyerSplit = new LinkedHashMap<>(); + Map byKind = new LinkedHashMap<>(); for (JDOrder o : rows) { if (!isCountEnabled(o)) { continue; } orderCount++; + String markName = blankTo(o.getDistributionMark(), "未标记"); + String kind = markKind(markName); + KindAcc ka = byKind.computeIfAbsent(kind, k -> new KindAcc()); + ka.orderCount++; boolean refunded = o.getIsRefunded() != null && o.getIsRefunded() == 1; if (refunded) { refundCount++; + ka.refundCount++; continue; } BigDecimal pay = money(o.getPaymentAmount()); @@ -318,14 +477,23 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi rebate = rebate.add(reb); extraCost = extraCost.add(extra); selling = selling.add(sell); + ka.payment = ka.payment.add(pay); + ka.rebate = ka.rebate.add(reb); + ka.extraCost = ka.extraCost.add(extra); + ka.selling = ka.selling.add(sell); if (o.getProfit() == null) { missingProfitCount++; + ka.missingProfit++; } else { - profit = profit.add(money(o.getProfit())); + BigDecimal pft = money(o.getProfit()); + profit = profit.add(pft); + ka.profit = ka.profit.add(pft); } - addGroup(byMark, blankTo(o.getDistributionMark(), "未标记"), pay, reb, extra, o.getProfit()); - addGroup(byModel, modelKey(o), pay, reb, extra, o.getProfit()); - addGroup(byBuyer, buyerGroupKey(o.getBuyer()), pay, reb, extra, o.getProfit()); + addGroup(byMark, markName, pay, reb, extra, o.getProfit(), kind); + addGroup(byModel, modelKey(o), pay, reb, extra, o.getProfit(), null); + addGroup(byBuyer, buyerGroupKey(o.getBuyer()), pay, reb, extra, o.getProfit(), null); + addSplit(byModelSplit, modelKey(o), kind, pay, reb, extra, o.getProfit()); + addSplit(byBuyerSplit, buyerGroupKey(o.getBuyer()), kind, pay, reb, extra, o.getProfit()); } int valid = orderCount - refundCount; @@ -351,9 +519,12 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi ? profit.multiply(BigDecimal.valueOf(100)).divide(payment, 2, RoundingMode.HALF_UP) : BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); out.put("profitRatePct", num(rate)); + out.put("byKind", toKindList(byKind)); out.put("byMark", toGroupList(byMark, Integer.MAX_VALUE)); out.put("byModel", toGroupList(byModel, TOP_N)); out.put("byBuyer", toGroupList(byBuyer, TOP_N)); + out.put("byModelSplit", toGroupList(byModelSplit, TOP_N)); + out.put("byBuyerSplit", toGroupList(byBuyerSplit, TOP_N)); return out; } @@ -389,6 +560,83 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi out.put(key, item); } + private static List> compareKinds(Map cur, Map prev) { + Map> a = indexKind(cur); + Map> b = indexKind(prev); + List names = new ArrayList<>(Arrays.asList("自有单", "代下单", "代下单单")); + if (kindHasOrders(a.get("未标记")) || kindHasOrders(b.get("未标记"))) { + names.add("未标记"); + } + List> out = new ArrayList<>(); + for (String name : names) { + Map ca = a.get(name); + Map cb = b.get(name); + Map row = new LinkedHashMap<>(); + row.put("name", name); + fillKindDelta(row, "validOrderCount", "validDelta", "validChangePct", ca, cb); + fillKindDelta(row, "totalProfit", "profitDelta", "profitChangePct", ca, cb); + BigDecimal rateA = toDecimal(ca == null ? null : ca.get("profitRatePct")); + BigDecimal rateB = toDecimal(cb == null ? null : cb.get("profitRatePct")); + row.put("rateDelta", num(rateA.subtract(rateB).setScale(2, RoundingMode.HALF_UP))); + out.add(row); + } + return out; + } + + private static void fillKindDelta(Map row, String src, String deltaKey, String pctKey, + Map cur, Map prev) { + BigDecimal a = toDecimal(cur == null ? null : cur.get(src)); + BigDecimal b = toDecimal(prev == null ? null : prev.get(src)); + BigDecimal delta = a.subtract(b).setScale(2, RoundingMode.HALF_UP); + row.put(deltaKey, num(delta)); + row.put(pctKey, num(changePct(a, b, delta))); + } + + private static BigDecimal changePct(BigDecimal current, BigDecimal previous, BigDecimal delta) { + if (previous.compareTo(BigDecimal.ZERO) == 0) { + if (current.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + } + return BigDecimal.valueOf(100).setScale(2, RoundingMode.HALF_UP); + } + return delta.multiply(BigDecimal.valueOf(100)).divide(previous.abs(), 2, RoundingMode.HALF_UP); + } + + private static boolean kindHasOrders(Map row) { + return row != null && toDecimal(row.get("orderCount")).compareTo(BigDecimal.ZERO) > 0; + } + + @SuppressWarnings("unchecked") + private static Map> indexKind(Map period) { + Map> out = new LinkedHashMap<>(); + if (period == null || !(period.get("byKind") instanceof List)) { + return out; + } + for (Object item : (List) period.get("byKind")) { + Map row = castMap(item); + if (row != null && row.get("name") != null) { + out.put(String.valueOf(row.get("name")), row); + } + } + return out; + } + + private String renderPrompt(String statsJson) { + String prompt = loadPrompt(); + String body = statsJson; + if (prompt.contains("current.totalProfit") || prompt.contains("compare.*")) { + body = "下面 JSON 已改为列数组。忽略上文 current/previous/compare 的对象字段说明。\n" + + "自有单、代下单(仅 F-W / F-H)、代下单单的单量、利润、利润率直接读 kind 和 kd,不要自己把 byMark 加总,也不要合并。\n" + + "列名以 _n、_amt、_row、_kind、_d、_kd 为准。\n" + + statsJson; + log.warn("慢单利润分析提示词仍是旧字段说明,已在数据前附加列数组说明。请用新模板覆盖 Redis 里的 order:profit_analysis。"); + } + if (prompt.contains("{{stats_json}}")) { + return prompt.replace("{{stats_json}}", body); + } + return prompt + "\n\n统计数据:\n" + body; + } + private String loadPrompt() { if (redisTemplate != null) { try { @@ -571,6 +819,20 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi sb.append("付款 ").append(cur.get("totalPayment")) .append(formatCmp(cmp, "totalPayment")) .append(" 后返 ").append(cur.get("totalRebate")).append('\n'); + List> kinds = (List>) cur.get("byKind"); + if (kinds != null && !kinds.isEmpty()) { + sb.append("分类型:"); + int n = 0; + for (Map k : kinds) { + if (n > 0) { + sb.append(";"); + } + sb.append(k.get("name")).append(' ').append(k.get("validOrderCount")).append("单/") + .append(k.get("totalProfit")).append("(").append(k.get("profitRatePct")).append("%)"); + n++; + } + sb.append('\n'); + } List> marks = (List>) cur.get("byMark"); if (marks != null && !marks.isEmpty()) { sb.append("分销:"); @@ -703,9 +965,53 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi return t; } + static String markKind(String mark) { + if (mark == null) { + return "未标记"; + } + String m = mark.trim(); + if (m.isEmpty() || "未标记".equals(m)) { + return "未标记"; + } + if (markToken(m, "F-W") || markToken(m, "F-H")) { + return "代下单"; + } + if (m.regionMatches(true, 0, "F", 0, 1)) { + return "自有单"; + } + return "代下单单"; + } + + /** F-W / F-H 本身,或以 F-W-、F-H- 再带后缀。F-Hello 不算 F-H。 */ + private static boolean markToken(String mark, String token) { + if (mark.length() < token.length() || !mark.regionMatches(true, 0, token, 0, token.length())) { + return false; + } + if (mark.length() == token.length()) { + return true; + } + char next = mark.charAt(token.length()); + return next == '-' || next == '_'; + } + private static void addGroup(Map map, String name, BigDecimal pay, BigDecimal reb, - BigDecimal extra, Double profit) { + BigDecimal extra, Double profit, String kind) { GroupAcc acc = map.computeIfAbsent(name, k -> new GroupAcc()); + if (kind != null) { + acc.kind = kind; + } + bump(acc, pay, reb, extra, profit); + } + + private static void addSplit(Map map, String name, String kind, BigDecimal pay, BigDecimal reb, + BigDecimal extra, Double profit) { + GroupAcc acc = map.computeIfAbsent(name + "\u0001" + kind, k -> new GroupAcc()); + acc.displayName = name; + acc.kind = kind; + bump(acc, pay, reb, extra, profit); + } + + private static void bump(GroupAcc acc, BigDecimal pay, BigDecimal reb, BigDecimal extra, Double profit) { acc.orderCount++; acc.payment = acc.payment.add(pay); acc.rebate = acc.rebate.add(reb); @@ -715,6 +1021,45 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi } } + private static List> toKindList(Map map) { + List names = new ArrayList<>(Arrays.asList("自有单", "代下单", "代下单单")); + KindAcc unmarked = map.get("未标记"); + if (unmarked != null && unmarked.orderCount > 0) { + names.add("未标记"); + } + List> out = new ArrayList<>(); + for (String name : names) { + out.add(kindRow(name, map.get(name))); + } + return out; + } + + private static Map kindRow(String name, KindAcc src) { + KindAcc a = src == null ? new KindAcc() : src; + int valid = a.orderCount - a.refundCount; + Map row = new LinkedHashMap<>(); + row.put("name", name); + row.put("orderCount", a.orderCount); + row.put("refundCount", a.refundCount); + row.put("validOrderCount", valid); + row.put("missingProfitCount", a.missingProfit); + row.put("totalPayment", num(a.payment)); + row.put("totalRebate", num(a.rebate)); + row.put("totalCost", num(a.payment.subtract(a.rebate))); + row.put("totalSellingPrice", num(a.selling)); + row.put("totalExtraCost", num(a.extraCost)); + row.put("totalProfit", num(a.profit)); + BigDecimal avg = valid > 0 + ? a.profit.divide(BigDecimal.valueOf(valid), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + row.put("avgProfit", num(avg)); + BigDecimal rate = a.payment.compareTo(BigDecimal.ZERO) > 0 + ? a.profit.multiply(BigDecimal.valueOf(100)).divide(a.payment, 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + row.put("profitRatePct", num(rate)); + return row; + } + private static List> toGroupList(Map map, int limit) { List> list = new ArrayList<>(map.entrySet()); list.sort(Comparator @@ -728,7 +1073,10 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi } GroupAcc a = e.getValue(); Map row = new LinkedHashMap<>(); - row.put("name", e.getKey()); + row.put("name", a.displayName != null ? a.displayName : e.getKey()); + if (a.kind != null) { + row.put("kind", a.kind); + } row.put("orderCount", a.orderCount); row.put("payment", num(a.payment)); row.put("rebate", num(a.rebate)); @@ -906,10 +1254,23 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi } private static final class GroupAcc { + String displayName; + String kind; int orderCount; BigDecimal payment = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); BigDecimal rebate = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); BigDecimal extraCost = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); BigDecimal profit = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } + + private static final class KindAcc { + int orderCount; + int refundCount; + int missingProfit; + BigDecimal payment = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + BigDecimal rebate = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + BigDecimal extraCost = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + BigDecimal selling = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + BigDecimal profit = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + } }