MonthlySettlementPlanner.java 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package com.smqjh.agent.mcp;
  2. import com.fasterxml.jackson.databind.node.ArrayNode;
  3. import com.fasterxml.jackson.databind.node.ObjectNode;
  4. import java.time.LocalDate;
  5. import java.time.format.DateTimeFormatter;
  6. import java.util.List;
  7. import java.util.Locale;
  8. final class MonthlySettlementPlanner {
  9. private static final List<Enterprise> ENTERPRISES = List.of(
  10. new Enterprise(54, "1", "招商银行贵阳分行"),
  11. new Enterprise(55, "2", "中数未来"),
  12. new Enterprise(58, "5", "铜仁移动")
  13. );
  14. ObjectNode plan(String enterpriseInput, String month) {
  15. Enterprise enterprise = findEnterprise(enterpriseInput);
  16. if (enterprise == null) {
  17. throw new IllegalArgumentException("未找到月结企业: " + enterpriseInput);
  18. }
  19. LocalDate start = parseMonth(month);
  20. LocalDate end = start.plusMonths(1);
  21. ObjectNode root = Jsons.object();
  22. root.put("title", enterprise.name() + " " + month + " 月结处理计划");
  23. root.put("enterpriseName", enterprise.name());
  24. root.put("channelId", enterprise.channelId());
  25. root.put("channelNo", enterprise.channelNo());
  26. root.put("month", month);
  27. root.put("startTime", start + " 00:00:00");
  28. root.put("endTime", end + " 00:00:00");
  29. root.put("settlementScope", "企业用户定时月结;所有已付款订单;逻辑删除订单不参与。");
  30. ArrayNode exportRows = exports(enterprise, start, end);
  31. root.set("columns", Jsons.MAPPER.valueToTree(new String[] {"name", "method", "adminEndpoint", "params", "backendEvidence", "note"}));
  32. root.set("rows", exportRows);
  33. root.set("exports", exportRows);
  34. root.set("rules", rules());
  35. root.set("invoiceStates", invoiceStates());
  36. root.put("adminTenantRule", "顶级 admin 登录/调用不需要 tenantCode;非 admin 仍需要 tenantCode 或租户上下文。");
  37. root.put("mcpUsage", "后续可由 Agent 先调用本工具生成计划,再调用导出接口、只读 SQL 和 Excel 处理工具生成结算报告。");
  38. return root;
  39. }
  40. ObjectNode enterprises() {
  41. ObjectNode root = Jsons.object();
  42. ArrayNode rows = Jsons.MAPPER.createArrayNode();
  43. for (Enterprise enterprise : ENTERPRISES) {
  44. ObjectNode row = Jsons.object();
  45. row.put("channelId", enterprise.channelId());
  46. row.put("channelNo", enterprise.channelNo());
  47. row.put("enterpriseName", enterprise.name());
  48. row.put("settlementType", "企业用户定时月结");
  49. row.put("scope", "所有已付款订单,排除逻辑删除订单");
  50. rows.add(row);
  51. }
  52. root.put("title", "月结企业清单");
  53. root.set("columns", Jsons.MAPPER.valueToTree(new String[] {"channelId", "channelNo", "enterpriseName", "settlementType", "scope"}));
  54. root.set("rows", rows);
  55. root.put("rowCount", rows.size());
  56. root.put("note", "来自管理员确认的 sm_channel 月结企业配置;顶级 admin 不需要 tenantCode。");
  57. return root;
  58. }
  59. private ArrayNode exports(Enterprise enterprise, LocalDate start, LocalDate end) {
  60. ArrayNode exports = Jsons.MAPPER.createArrayNode();
  61. exports.add(exportSpec(
  62. "员工列表积分情况",
  63. "GET",
  64. "/plt/admin/enterprise/export",
  65. "channelIdList[0]=" + enterprise.channelId(),
  66. "业务代码证据:smqjh-system /api/v1/members/enterprise/export;积分充值表 sm_points_recharge;会员表 sm_member。",
  67. "用于员工、手机号、充值积分、消费积分、可用积分核对;汇总时按 user_id + channel_id。"
  68. ));
  69. exports.add(exportSpec(
  70. "订单表",
  71. "GET/POST",
  72. "/plt/platform/order/export",
  73. "channelIdList[0]=" + enterprise.channelId() + "&orderStatus=all&startTime=" + start + "%2000:00:00&endTime=" + end + "%2000:00:00",
  74. "业务代码证据:OrderController POST /api/v1/order/export?exportType=ORDER;OmsOrderMapper.exportOrderList;oms_order。",
  75. "导出后需要按 is_payed=1 和 COALESCE(delete_status,0)=0 口径核对,金额差异标记需人工确认。"
  76. ));
  77. exports.add(exportSpec(
  78. "商品订单表",
  79. "GET/POST",
  80. "/plt/platform/order/export",
  81. "channelIdList[0]=" + enterprise.channelId() + "&orderStatus=all&exportType=PRODUCT&startTime=" + start + "%2000:00:00&endTime=" + end + "%2000:00:00",
  82. "业务代码证据:OrderController POST /api/v1/order/export?exportType=PRODUCT;OmsOrderMapper.exportProductList;oms_order_item + tz_sku。",
  83. "从商品表补齐海博编码、商品编码、SKU 编码;汇总商品总额、数量和规格。"
  84. ));
  85. exports.add(exportSpec(
  86. "运费账单表",
  87. "GET",
  88. "/plt/platform/sku/freightStatisticsExcel",
  89. "channelIds[0]=" + enterprise.channelId(),
  90. "业务代码证据:运费导出入口由后台提供;优先按订单号和配送单号与 oms_order 关联。",
  91. "汇总总运费;订单号无法关联或金额不一致时标记需人工确认。"
  92. ));
  93. exports.add(exportSpec(
  94. "对账汇总表模板",
  95. "GET",
  96. "/plt/platform/sku/skuStatisticsExcel",
  97. "channelIds[0]=" + enterprise.channelId(),
  98. "业务代码证据:后台对账汇总导出模板;Agent 负责补充汇总公式和蓝色核对区。",
  99. "业务系统导出的模板本身可用,但缺少自动汇总计算和小蓝表核对区。"
  100. ));
  101. return exports;
  102. }
  103. private ObjectNode exportSpec(String name, String method, String endpoint, String params, String evidence, String note) {
  104. ObjectNode item = Jsons.object();
  105. item.put("name", name);
  106. item.put("method", method);
  107. item.put("adminEndpoint", endpoint);
  108. item.put("params", params);
  109. item.put("backendEvidence", evidence);
  110. item.put("note", note);
  111. return item;
  112. }
  113. private ArrayNode rules() {
  114. ArrayNode rules = Jsons.MAPPER.createArrayNode();
  115. rules.add("商品总额 = 已付款商品订单明细小计汇总。");
  116. rules.add("总运费 = 运费账单按订单号或配送单号匹配后汇总。");
  117. rules.add("负数积分抵扣表示充值转换为积分后的抵扣金额。");
  118. rules.add("商品金额、运费、积分抵扣、现金支付任意差异不为 0,标记为“需人工确认”。");
  119. rules.add("只标记差异,不自动修改业务系统数据。");
  120. rules.add("结算报告生成后发送客户核对,再打印盖章、开电子发票、等待客户付款。");
  121. return rules;
  122. }
  123. private ArrayNode invoiceStates() {
  124. ArrayNode states = Jsons.MAPPER.createArrayNode();
  125. states.add("待客户核对");
  126. states.add("待打印盖章");
  127. states.add("待开电子发票");
  128. states.add("待客户付款");
  129. states.add("已完成");
  130. return states;
  131. }
  132. private Enterprise findEnterprise(String input) {
  133. String normalized = input == null ? "" : input.trim().toLowerCase(Locale.ROOT);
  134. return ENTERPRISES.stream()
  135. .filter(item -> item.name().toLowerCase(Locale.ROOT).contains(normalized)
  136. || item.channelNo().equals(normalized)
  137. || String.valueOf(item.channelId()).equals(normalized))
  138. .findFirst()
  139. .orElse(null);
  140. }
  141. private LocalDate parseMonth(String month) {
  142. if (month == null || !month.matches("\\d{4}-\\d{2}")) {
  143. throw new IllegalArgumentException("month 必须是 YYYY-MM");
  144. }
  145. return LocalDate.parse(month + "-01", DateTimeFormatter.ISO_LOCAL_DATE);
  146. }
  147. private record Enterprise(int channelId, String channelNo, String name) {
  148. }
  149. }