package com.smqjh.agent.mcp; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Locale; final class MonthlySettlementPlanner { private static final List ENTERPRISES = List.of( new Enterprise(54, "1", "招商银行贵阳分行"), new Enterprise(55, "2", "中数未来"), new Enterprise(58, "5", "铜仁移动") ); ObjectNode plan(String enterpriseInput, String month) { Enterprise enterprise = findEnterprise(enterpriseInput); if (enterprise == null) { throw new IllegalArgumentException("未找到月结企业: " + enterpriseInput); } LocalDate start = parseMonth(month); LocalDate end = start.plusMonths(1); ObjectNode root = Jsons.object(); root.put("title", enterprise.name() + " " + month + " 月结处理计划"); root.put("enterpriseName", enterprise.name()); root.put("channelId", enterprise.channelId()); root.put("channelNo", enterprise.channelNo()); root.put("month", month); root.put("startTime", start + " 00:00:00"); root.put("endTime", end + " 00:00:00"); root.put("settlementScope", "企业用户定时月结;所有已付款订单;逻辑删除订单不参与。"); ArrayNode exportRows = exports(enterprise, start, end); root.set("columns", Jsons.MAPPER.valueToTree(new String[] {"name", "method", "adminEndpoint", "params", "backendEvidence", "note"})); root.set("rows", exportRows); root.set("exports", exportRows); root.set("rules", rules()); root.set("invoiceStates", invoiceStates()); root.put("adminTenantRule", "顶级 admin 登录/调用不需要 tenantCode;非 admin 仍需要 tenantCode 或租户上下文。"); root.put("mcpUsage", "后续可由 Agent 先调用本工具生成计划,再调用导出接口、只读 SQL 和 Excel 处理工具生成结算报告。"); return root; } ObjectNode enterprises() { ObjectNode root = Jsons.object(); ArrayNode rows = Jsons.MAPPER.createArrayNode(); for (Enterprise enterprise : ENTERPRISES) { ObjectNode row = Jsons.object(); row.put("channelId", enterprise.channelId()); row.put("channelNo", enterprise.channelNo()); row.put("enterpriseName", enterprise.name()); row.put("settlementType", "企业用户定时月结"); row.put("scope", "所有已付款订单,排除逻辑删除订单"); rows.add(row); } root.put("title", "月结企业清单"); root.set("columns", Jsons.MAPPER.valueToTree(new String[] {"channelId", "channelNo", "enterpriseName", "settlementType", "scope"})); root.set("rows", rows); root.put("rowCount", rows.size()); root.put("note", "来自管理员确认的 sm_channel 月结企业配置;顶级 admin 不需要 tenantCode。"); return root; } private ArrayNode exports(Enterprise enterprise, LocalDate start, LocalDate end) { ArrayNode exports = Jsons.MAPPER.createArrayNode(); exports.add(exportSpec( "员工列表积分情况", "GET", "/plt/admin/enterprise/export", "channelIdList[0]=" + enterprise.channelId(), "业务代码证据:smqjh-system /api/v1/members/enterprise/export;积分充值表 sm_points_recharge;会员表 sm_member。", "用于员工、手机号、充值积分、消费积分、可用积分核对;汇总时按 user_id + channel_id。" )); exports.add(exportSpec( "订单表", "GET/POST", "/plt/platform/order/export", "channelIdList[0]=" + enterprise.channelId() + "&orderStatus=all&startTime=" + start + "%2000:00:00&endTime=" + end + "%2000:00:00", "业务代码证据:OrderController POST /api/v1/order/export?exportType=ORDER;OmsOrderMapper.exportOrderList;oms_order。", "导出后需要按 is_payed=1 和 COALESCE(delete_status,0)=0 口径核对,金额差异标记需人工确认。" )); exports.add(exportSpec( "商品订单表", "GET/POST", "/plt/platform/order/export", "channelIdList[0]=" + enterprise.channelId() + "&orderStatus=all&exportType=PRODUCT&startTime=" + start + "%2000:00:00&endTime=" + end + "%2000:00:00", "业务代码证据:OrderController POST /api/v1/order/export?exportType=PRODUCT;OmsOrderMapper.exportProductList;oms_order_item + tz_sku。", "从商品表补齐海博编码、商品编码、SKU 编码;汇总商品总额、数量和规格。" )); exports.add(exportSpec( "运费账单表", "GET", "/plt/platform/sku/freightStatisticsExcel", "channelIds[0]=" + enterprise.channelId(), "业务代码证据:运费导出入口由后台提供;优先按订单号和配送单号与 oms_order 关联。", "汇总总运费;订单号无法关联或金额不一致时标记需人工确认。" )); exports.add(exportSpec( "对账汇总表模板", "GET", "/plt/platform/sku/skuStatisticsExcel", "channelIds[0]=" + enterprise.channelId(), "业务代码证据:后台对账汇总导出模板;Agent 负责补充汇总公式和蓝色核对区。", "业务系统导出的模板本身可用,但缺少自动汇总计算和小蓝表核对区。" )); return exports; } private ObjectNode exportSpec(String name, String method, String endpoint, String params, String evidence, String note) { ObjectNode item = Jsons.object(); item.put("name", name); item.put("method", method); item.put("adminEndpoint", endpoint); item.put("params", params); item.put("backendEvidence", evidence); item.put("note", note); return item; } private ArrayNode rules() { ArrayNode rules = Jsons.MAPPER.createArrayNode(); rules.add("商品总额 = 已付款商品订单明细小计汇总。"); rules.add("总运费 = 运费账单按订单号或配送单号匹配后汇总。"); rules.add("负数积分抵扣表示充值转换为积分后的抵扣金额。"); rules.add("商品金额、运费、积分抵扣、现金支付任意差异不为 0,标记为“需人工确认”。"); rules.add("只标记差异,不自动修改业务系统数据。"); rules.add("结算报告生成后发送客户核对,再打印盖章、开电子发票、等待客户付款。"); return rules; } private ArrayNode invoiceStates() { ArrayNode states = Jsons.MAPPER.createArrayNode(); states.add("待客户核对"); states.add("待打印盖章"); states.add("待开电子发票"); states.add("待客户付款"); states.add("已完成"); return states; } private Enterprise findEnterprise(String input) { String normalized = input == null ? "" : input.trim().toLowerCase(Locale.ROOT); return ENTERPRISES.stream() .filter(item -> item.name().toLowerCase(Locale.ROOT).contains(normalized) || item.channelNo().equals(normalized) || String.valueOf(item.channelId()).equals(normalized)) .findFirst() .orElse(null); } private LocalDate parseMonth(String month) { if (month == null || !month.matches("\\d{4}-\\d{2}")) { throw new IllegalArgumentException("month 必须是 YYYY-MM"); } return LocalDate.parse(month + "-01", DateTimeFormatter.ISO_LOCAL_DATE); } private record Enterprise(int channelId, String channelNo, String name) { } }