# 仓储 / 采购 / 财务 / 项目 数据类 MCP 工具方案(v15 · 已实现并端到端验证) > 状态:**已实现(13 个新工具,共 15 个),编译通过 + 认证态端到端 34/34 用例通过** > 适用范围:`ruoyi-sip` 模块 `com.ruoyi.sip.llm.tools` 下的只读查询工具 > 关联文档:[prompt.md](./prompt.md)(MCP Server 原始规格)、[mcp-tools-index-ddl.sql](./mcp-tools-index-ddl.sql)(索引核对与补建脚本) ## 最终方案速览(TL;DR) | 项 | 结论 | |---|---| | 工具数 | **13 个** = 标识符点查 3 + 聚合 3 + 列表/范围 5 + 扩展 2(`project_list`、`cross_domain_aggregate`) | | 覆盖 | 仓储 / 采购 / 财务 / **项目(含 POC、进度、报价)** 四域 + **manage 域合同与合同明细** + **审批待办与已办**,含基础列表能力、**签收**、**撤回历史**、时间维度、计收、备货、主数据批量翻译(**含仓库与己方公司主体**)、**受限跨域透视(客户×产品×月 等)**、**账龄分桶**、**财务历史时点余额** | | 性能 | 过滤键实测对齐索引;游标分页不重不漏;聚合默认 `mode=SUMMARY` 一次算完;类型对齐防索引退化;查询超时 + 限流;**RAG 路由控制 schema token** | | 统计维度 | 产品 / 仓库 / 订单 / 采购单 / 供应商 / 合作伙伴 / 代表处 / 项目 / 状态 / 时间(月、季)/ **账龄分桶** / 无维度(`OWNER` 已移除,见 15.2) | | 索引 | 必加 2(P0)+ 建议 3(P1,含覆盖索引)+ 条件加 23(P2 编号 26 条,其中 3 条已作废,按表规模触发) | | 不改动 | **不改**数据库字段、**不改**任何业务写入逻辑、**不改**现有 2 个工具与 MCP 框架(`McpService`/`McpToolRegistry`/`ToolInitializer`/`McpController`) | | 明确不做 | 写操作、**财务附件的文件内容(仅元数据)**、财务运营报表物化表(改用实时聚合 + 时点重算)、审计日志、报表导出文件、任意 SQL / 无白名单的任意维度 | | 业务口径 | 已**配置化落地**(在库口径、价格含税口径),业务跑一次校验 SQL 后改配置即可,不阻塞编码(见 16.3) | | 工程项 | 索引 DDL 执行方案(16.4)、时间区间策略(16.5)、从库路由(16.6)、**RAG 工具路由(16.7,13 个工具下为必须)** | --- ## 一、目标与范围 ### 1.1 目标 在现有 MCP Server(已有 `project_order_info`、`product_info` 两个工具)基础上,新增覆盖**仓储、采购、财务**三个数据域的只读查询工具,满足: 1. 覆盖高频业务问题,且**一次调用尽量拿全**,减少 Agent 轮次; 2. 单次调用**快**:过滤键全部命中索引,不产生 N+1 与全表扫描; 3. **数据不缺失**:范围类查询支持游标分页,由 Agent 自行翻页直至取完; 4. **不越权**:与页面入口的数据权限、菜单权限对齐; 5. **不改数据库字段、不改业务写入逻辑**:仅新增只读 SQL 与索引。 ### 1.2 范围外(明确不做) - 任何写操作(新增/修改/删除/审批/撤回/红冲); - 基于 `oms_finance_operate_report` 的工具(原因见 2.1); - 采购/财务**审批待办与已办**(`bu_todo` 相关,属流程域)、财务附件内容; - 改造现有两个工具的行为。 --- ## 二、前置验证结论(实测,非推断) 验证环境:`oms_test`(`192.168.124.202:3307`),MySQL **8.0.37**。 ### 2.1 财务物化表不可用 —— 已否决原设计 | 项 | 实测结论 | |---|---| | 刷新入口 | 全仓仅 `GET /test`,且带 `@Anonymous`(免鉴权),属临时手工触发口 | | 定时任务 | **无**(全仓无 `@Scheduled` / quartz job 引用 `statisticsReport()`) | | 刷新方式 | 增量水位制:以 `maxDataDate()` 为 watermark(`OmsFinanceOperateReportServiceImpl#statisticsReport`) | | 数据现状 | `oms_finance_operate_report` 与 `oms_finance_operate_report_detail` **均为 0 行** | **结论**:该表是人工触发、且漏触发会永久偏差的物化表,**不能作为 MCP 财务数据源**。财务工具一律走实时单据表聚合。 ### 2.2 数据量级(`oms_test`,仅作参考) | 表 | 行数 | 表 | 行数 | |---|---|---|---| | `oms_inventory_info` | **64,823**(精确) | `oms_inventory_delivery` | 756 | | `oms_inventory_delivery_detail` | 49,015(估) | `oms_inventory_outer_detail` | 750 | | `oms_purchase_order_item` | 951 | `oms_inventory_outer` | 734 | | `oms_purchase_order` | 941 | `oms_receivable_bill` | 627 | | `oms_inventory_inner` | 580 | `oms_payable_bill` | 583 | | `oms_stock_info` | 526 | `oms_payment_bill` | 64 | | `oms_ticket_bill` | 21 | `oms_warehouse_info` | 14 | | `oms_receipt_bill` | 1 | `oms_invoice_bill` | 0 | **结论**:按**百万级**保守设计,约束集中在 `oms_inventory_info` 与 `oms_inventory_delivery_detail` 两张表。 ### 2.3 `oms_inventory_info` 关键事实 - 主键 `id` 为 **int**(非 bigint),游标可安全编码; - `inventory_status` 取值实测仅 `'0'`(在库 15,532) / `'1'`(出库 49,291); - `tax_rate` 实测**存在 NULL**,未税换算需兜底为 0; - 表结构含 `product_code / product_sn / inner_code / outer_code / warehouse_id / inner_price / outer_price / order_code / payable_bill_code`。 ### 2.4 列存在性验证(用于索引 DDL 可行性) | 表 | 关键列存在情况 | |---|---| | `oms_inventory_outer` | 有 `outer_code`、`order_code`;**无 `warehouse_id`** | | `oms_inventory_outer_detail` | 有 `outer_code`、`warehouse_id` | | `oms_inventory_inner` | 有 `inner_code`、`order_code`、`purchase_no` | | `oms_inventory_delivery` | 有 `outer_code`;**无 `order_code`、无 `product_code`**(均来自 join) | | `oms_inventory_delivery_detail` | 有 `delivery_id`、`product_sn` | | `oms_purchase_order` | 有 `purchase_no`、`vendor_id`、`status`、`purchase_date` | | `oms_purchase_order_item` | 有 `purchase_id`、`product_code`、`inner_quantity` | | `oms_purchase_order_map` | 有 `order_id`、`purchase_id`、`product_code`、`bind_num` | > **重要**:`oms_inventory_delivery.order_code` 不是本表列,**不能对其建索引**;按订单号查发货单必须经 `oms_inventory_outer.order_code` 关联(该列已有索引)。 ### 2.5 现有索引实测(与本方案相关的部分) | 表 | 现有索引 | 是否满足需求 | |---|---|---| | `oms_inventory_info` | `product_sn`(UNIQUE)、`inner_code`、`outer_code`、`product_code` | ✅ 满足;`warehouse_id` 无索引 | | `oms_inventory_inner` | `inner_code`(UNIQUE) | ⚠️ `order_code` 无索引 | | `oms_inventory_delivery` | `outer_code` | ✅ | | `oms_inventory_delivery_detail` | `delivery_id`、`product_sn` | ✅ | | **`oms_inventory_outer`** | 仅 `order_code`,**`outer_code` 无索引** | ❌ **需新增** | | **`oms_inventory_outer_detail`** | 仅主键 | ❌ **需新增** | | `oms_receivable_bill` | `receivable_bill_code`(UNIQUE)、`order_code`、`inventory_code` | ✅ | | `oms_payable_bill` | `payable_bill_code`(UNIQUE)、`order_code`、`inventory_code` | ✅ | | `oms_receipt_bill` / `oms_payment_bill` | 单号 UNIQUE | ✅ | | `oms_invoice_bill` / `oms_ticket_bill` | 单号 UNIQUE | ✅ | | 收/付/开票/收票 计划与明细表 | `receivable_bill_id`、`payable_bill_id`、`*_plan_id`、`*_bill_code` 均有索引 | ✅ | | `oms_purchase_order` | 仅 `purchase_no`(UNIQUE) | ⚠️ `vendor_id`/`status`/时间无索引 | | `oms_purchase_order_item` | `purchase_id` | ✅ | | `oms_purchase_order_map` | 仅主键 | ⚠️ `order_id`/`purchase_id` 无索引 | | `oms_stock_info` | 仅主键 | ⚠️ 未纳入本方案 | | 全部表 | **无任何时间列索引** | ❌ 时间范围不作主过滤键 | **补充**:代码中 `oms_receivable_write_off_detail` / `oms_payable_write_off_detail` 两个 domain 实际不参与查询(库中无此表),核销真实链路为 `receipt_detail.write_off_id` / `payment_detail.write_off_id`;这两列**无索引**,因此工具**不支持按核销单号反查**(已规避)。 --- ## 三、设计原则 1. **面向问题,而非面向表**:一个工具回答一类业务问题,避免"一表一工具"导致工具数膨胀、Agent 选错率与 schema token 上升。 2. **点查不翻页、范围必翻页**:入参为单号/SN 精确值时结果有界,一次返回;入参为条件/聚合时强制游标分页。 3. **过滤键必须命中索引**:每个工具的必填入口键都对应一个实测存在的索引;无索引的维度不开放为入口。 4. **消除 N+1**:一律"先查主表取键集合 → `in (...)` 批量查子表 → 内存分组"。 5. **权限前置**:`handle` 第一行做菜单权限校验,再按页面口径回填行级权限过滤。 6. **只读**:不开启事务、不写库、不触发业务副作用。 7. **口径显式**:聚合口径、字段含义写入 `metadata`,避免 Agent 把口径差异误判为"数据缺失"。 ### 3.8 与现有 MCP 工具的一致性基线(实现前必须先明确) 本方案**不是另起一套,而是完全沿用现有工具的范式**。现有两个工具:[ProjectOrderInfoToolProvider.java](../ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProjectOrderInfoToolProvider.java)、[ProductInfoToolProvider.java](../ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProductInfoToolProvider.java),公共基类 [AbstractMcpToolProvider.java](../ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/support/AbstractMcpToolProvider.java)。 | 维度 | 现有实现 | 本方案 | |---|---|---| | 注册方式 | `@Component` + `extends AbstractMcpToolProvider`;[ToolInitializer.java](../ruoyi-sip/src/main/java/com/ruoyi/sip/llm/ToolInitializer.java) 启动时扫描 `List` 自动注册 | **一致**:只新增工具类,**不改** `ToolInitializer` / `McpToolRegistry` / `McpService` / `McpController` | | 工具命名 | `snake_case`(`project_order_info`、`product_info`) | 一致 | | 工具描述 | `getToolDescription()` 返回中文一句话 | 一致;分页工具在描述末尾追加 Agent 翻页指令(见 5.6) | | 入参 Schema | `objectSchema(properties, required...)` + `stringProperty("中文说明")`,`additionalProperties=false` | 一致;基类扩展 `integerProperty` / `arrayProperty` / `enumProperty`(中文说明) | | 返回结构 | `response(metadata, query, data)`;`data.total` + `data.items`;items 字段为**英文驼峰** | 一致;仅新增 `data.page_info` | | 字段注释 | `metadata.item_fields` 中文键值对(见 `buildItemFieldMetadata()`) | 一致;每个工具必须提供完整字段字典(**附录 A**) | | 字典翻译 | `DictUtils.getDictLabel(dictType, code)` | 一致;枚举类字段用 `XxxEnum#getValue()`,两类来源在附录 A.13 区分 | | 批量加载 | 先查主表取编码集合 → `in (...)` 批量 → `groupingBy` 组装(`ProjectOrderInfoToolProvider#loadShipmentSummaryMap` 即此写法) | 一致 | | 日期格式 | `DateUtil.format(value, "yyyy-MM-dd")` / `DateUtils.YYYY_MM_DD_HH_MM_SS` | 一致 | | 空值处理 | 固定字段集全量输出(空值输出 null 或空串) | 一致(**不采用**"空值字段不输出"的裁剪方式,避免模型误判字段缺失) | | 错误 | `throw new RuntimeException("英文消息")`,由 `McpController#toMcpError` 映射 | 一致,不新增错误类型 | ### 3.9 对现有写法的有意偏离(共 4 处,均有理由) | # | 偏离 | 理由 | |---|---|---| | 1 | 新增分页(`data.page_info` + 游标) | 现有工具无分页能力,`project_order_info` 按时间范围查询可能返回超大结果集。**`data.total` 键予以保留以兼容现有约定**:默认 `null`,`include_total=true` 时填真实值 | | 2 | 新增菜单/行级权限校验 | 现有工具依赖 Service 层 `@DataScope`;但仓储与采购的**行级权限在 Controller 层**(`IInventoryAuthService`),直调 Service 会越权,必须显式对齐 | | 3 | 不使用动态列 | 现有 `project_order_info` 用 `softwareCode1/2/3…` + `dynamic_field_rules` 表达明细行,对模型不友好;新工具统一用**嵌套数组**(`items[].subItems[]`) | | 4 | 新增 `metadata.dict_fields` / `aggregation_rule` | 用于向 Agent 声明字典取值来源与聚合口径,减少"口径误解被当成数据缺失" | --- ## 四、工具清单(13 个,最终版) > 划分原则:**① 标识符点查**(有界,一次拿全,不分页)→ **② 聚合**(默认 `mode=SUMMARY` 一次算完)→ **③ 列表/范围查询**(游标分页)。 > **版本演进**:v6 拟定的 `purchase_order_detail`、`finance_bill_detail` 已**合并**进 `purchase_list` / `finance_list`(用 `code_list` + `include_detail=true` 表达点查),工具数由 11 收敛为 9;v8 纳入主数据工具 `master_data_list`(`warehouse_list` 扩到 7 个 entity)→ 10 个;v9 纳入 `project_list` 与 `cross_domain_aggregate` → 12 个;**v13 纳入 `approval_list`(审批待办/已办)→ 13 个**。 ### 4.1 A 类:标识符点查(不分页)3 个 | # | 工具名 | 必填标识符 | 返回 | 数据链路 | 索引支撑 | |---|---|---|---|---|---| | 1 | `inventory_sn_trace` | `product_sn_list`(≤50) / `inner_code_list`(≤20) / `outer_code_list`(≤20) 三选一 | SN 明细:库存状态、入库价/出库价(**含税口径待确认**,见 15.8)、税率、所属入库单/出库单/合同号、仓库 | `oms_inventory_info` 直查 | `product_sn`(UK)、`inner_code`、`outer_code` ✅ | | 2 | `inventory_flow` | `outer_code` 或 `order_code` | **6 分组**:`inner`、`outer`、`outerDetails`、`deliveries`、`snDetails`、**`stock`(新增·备货状态)** | 入库单 + 出库单(±明细) + 发货单(±明细) + SN + **`oms_stock_info`** | 依赖 P0-1/P0-2;`stock` 按 `order_code`(小表) | | 3 | `finance_order_position` | `order_code` | **14 分组**:receivable、receiptPlans、invoicePlans、receipts、receiptWriteOffs、invoices、payable、paymentPlans、ticketPlans、payments、tickets、paymentWriteOffs、ticketWriteOffs、**`charge`(新增·计收)** | 应收/应付 + 计划/明细/核销 + 收付票单 + **`oms_finance_charge`** | 各单号 UNIQUE、`order_code`、**`oms_finance_charge.order_code`(UK)** ✅ | > 子列表(如某订单的收款单条数很多)超单页上限时,返回该子列表的 `sub_cursor` 局部游标,仅供该子列表翻页。 ### 4.2 B 类:聚合(`mode=SUMMARY` 默认,`LIST` 可选)3 个 | # | 工具名 | `group_by` 可选值 | 度量 | 数据来源 | |---|---|---|---|---| | 4 | `inventory_stock_aggregate` | `PRODUCT` / `WAREHOUSE` / `PRODUCT_WAREHOUSE` / **`STATUS`** / **`TIME_MONTH`** / **`TIME_QUARTER`** / `NONE` | `in_stock_qty`、`out_stock_qty`、`inner_amount`、`outer_amount` | `oms_inventory_info` | | 5 | `purchase_arrival_aggregate` | `ORDER`(采购单号) / **`VENDOR`** / **`PRODUCT`** / **`STATUS`** / **`TIME_MONTH`** / `NONE` | `purchase_qty`、`inner_qty`、`pending_qty`、`arrival_rate`、**`arrival_delay_days`**、`amount_total`、`tax_total` | `oms_purchase_order` ⋈ `oms_purchase_order_item` | | 6 | `finance_balance_aggregate` | `ORDER` / **`PARTNER`** / **`STATUS`** / **`TIME_MONTH`** / `NONE` | 5 流余额(应收/收款/开票/应付/付款/收票)+ **`overdue_days`** | `oms_receivable_bill` / `oms_payable_bill` | > 分页粒度选择理由:`oms_inventory_info` 索引顺序为 `(product_code, id)`,以产品为页边界可沿用同一条索引;按"产品+仓库"组合分页需跨索引排序,成本更高。 > **重要**:聚合工具必须同时具备"汇总模式"与"明细模式",否则全局统计只能靠 Agent 翻页累加,会退化为多次全表扫描且可能被 `max_pages` 截断。详见 **第十三章**。 ### 4.3 C 类:列表 / 范围查询(游标分页)5 个 | # | 工具名 | `entity` 可选值 | 过滤维度 | 明细 | |---|---|---|---|---| | 7 | `warehouse_list` | `INNER` / `OUTER` / `DELIVERY` / **`ORDER_DELIVERY`(manage 域发货单,含签收)** / `STOCK` / `SN` / **`RECALL`(撤回历史)** | 单号、状态、时间范围、仓库、产品、合同号 | `include_detail=true` 返回明细(来源见下表) | | 8 | `purchase_list` | `ORDER` / `ITEM` / `HISTORY` / `ORDER_BIND` | 单号、状态、审批/确认状态、时间范围、供应商、产品 | `include_detail=true` 返回明细行 | | 9 | `finance_list` | `RECEIVABLE` / `PAYABLE` / `RECEIPT` / `PAYMENT` / `INVOICE` / `TICKET` / `CHARGE` / **`ATTACHMENT`(v13 新增,仅元数据)** | 单号、状态、审批状态、时间范围、合作伙伴 | `include_detail=true` 返回计划/明细/核销 | | 10 | **`master_data_list`** | `PARTNER` / `CUSTOMER` / `AGENT` / `VENDOR` / `PRODUCT` / `USER` / **`WAREHOUSE`(v10 新增)** / **`COMPANY`(v10 新增)** | `code_list`(**批量,≤200**)、名称模糊、类型/状态 | 无明细(主数据) | | 11 | **`approval_list`(v13 新增)** | **`TODO`(待办)/ `DONE`(已办)** | 审批人(默认当前登录人)、`process_key`、业务主键 `business_key`、时间范围 | 无明细(`TODO` 返回当前节点;`DONE` 返回审批意见与结果) | > **`approval_list` 的价值**:回答"我还有哪些单要审""现在卡在谁那儿""**为什么被驳回**""审批耗了多久"。数据来自 `bu_todo`(61) 与 `bu_todo_completed`(5,876) 两张**业务表**,**不需要读 Flowable 的 `act_*`**(实测两表已冗余 `business_key`/`process_key`/`task_name`/`approve_user_name`/`apply_time`/`approve_opinion`/`approve_status`)。已覆盖流程:`order_approve_online/offline`、`purchase_order_online`、`finance_payment`、`fianance_ticket`、`order_reback`、`outer_reback`。 > `master_data_list` 的作用:解决"Agent 拿到 `partner_code`/`vendor_code`/`product_code` 却查不到名称"的关联实体缺口(原 `purchase_list(entity=VENDOR)` 已并入本工具的 `VENDOR`,避免重复)。 **列表工具的护栏**: 1. `entity=SN` 走**大表**(`oms_inventory_info` 6.5 万行)→ 必须给出 `product_sn_list` / `inner_code_list` / `outer_code_list` / `product_code_list` 之一,否则返回 `INVALID_PARAMS`; 2. 其余 entity 所在表当前均 < 1000 行 → 允许状态/时间/合作伙伴维度直接过滤(依据 14.4 的**表规模分级**规则),并在 `metadata` 注明"该表当前规模小,增长后需补索引"(见 P2); 3. `include_detail` 默认 `false`(列表只要表头,省 token);需要完整明细树时置 `true`; 4. 单据点查用 `code_list` + `include_detail=true`。 **各 entity 的明细来源(v10 修正:入库明细以实测数据为准)** | entity | 明细子表 | 说明 | |---|---|---| | `INNER` | **`oms_inventory_info`(按 `inner_code`)** | **实测:入库明细实际落在 SN 表**——`oms_inventory_info` 中 579/580 张入库单有 SN 明细(样例 `R-20250917001` 有 500 条 SN)。`oms_inventory_inner_detail` 表虽定义存在,但**实测仅 1 行(未启用)**,不采用 | | `OUTER` | `oms_inventory_outer_detail` | 按仓库拆分的出库数量 | | `DELIVERY` | `oms_inventory_delivery_detail` | 仓储域发货 SN 明细 | | `ORDER_DELIVERY` | **`delivery_list`** | manage 域发货 SN 明细(`delivery_id` + `serial_number`),**必须过滤 `deleted_at is null`**。另:本 entity 的 `orderId` 指向 **manage 域合同表 `order_info`**(实测 355/355 全部命中),**必须 join `order_info` 才能输出合同编号/客户/代理商**(且需 `trim()`,见 15.14) | | `STOCK` / `SN` / `RECALL` | 无 | — | > 另有 `oms_inventory_inner_maintenance`(维保入库,当前 0 行)**不纳入**本轮范围。 > ⚠️ **口径提醒(v10 实测发现)**:`oms_inventory_info.order_code` 在 SN **未出库时为空**——实测 64,823 行中 15,532 行为空,且该数量**恰好等于在库数量**(`inventory_status='0'`)。因此**不能用 `inventory_info.order_code` 反查"在库货物属于哪个订单"**,必须经 `inner_code` → `oms_inventory_inner.order_code`。 ### 4.4 D 类:扩展能力(v9 新增)2 个 | # | 工具名 | 用途 | 详见 | |---|---|---|---| | 12 | **`project_list`** | 项目 / 项目产品 / 项目进度 / POC / 报价单 / **manage 域合同与合同明细(v11 新增)**(entity 参数化) | **16.1**(含字段字典与索引) | | 13 | **`cross_domain_aggregate`** | **受限跨域透视**:"客户 × 产品 × 月"等组合分析(维度/度量白名单 + 单链路约束) | **16.2**(含白名单、链路与护栏) | > 这两个工具的字段字典直接写在第十六章对应小节(避免与附录 A 重复)。附录 A 收录其余 11 个工具的字段字典(含 v13 新增的 `approval_list`,见 A.15)。 ### 4.5 统一响应契约(对齐现有工具,仅新增 `page_info`) 完全沿用现有 `AbstractMcpToolProvider#response(metadata, query, data)`;字段注释一律放 `metadata.item_fields`(与现有工具写法一致): ``` { "metadata": { "tool": "...", // 工具名(与现有工具一致) "description": "...", // 中文说明 "query_fields": { "<入参>": "<中文注释>" }, // 入参字段注释 "data_fields": { "total": "...", "items": "...", "page_info": "..." }, "item_fields": { "<返回字段>": "<中文注释>" }, // ★ 字段注释(附录 A 为权威来源) "dict_fields": { "<返回字段>": "<字典类型或枚举类>" }, "aggregation_rule": { ... } // 仅聚合类工具提供 }, "query": { ...规范化后的入参回显... }, "data": { "total": null, "items": [ ... ], "page_info": { ... } } } ``` - `data.total`:**保留现有约定**;默认 `null`(不额外 count),`include_total=true` 时填真实值,超 `count_cap` 时并置 `total_count_capped=true`; - `metadata.item_fields` **必填**:每个工具都要有完整中文字段注释(对齐现有 `buildItemFieldMetadata()` 的做法,内容取附录 A); - `metadata.dict_fields`:声明哪些字段做了翻译、来源是字典表还是枚举类,便于 Agent 理解取值; - 错误契约:沿用 `McpErrorUtils` 的 `INVALID_PARAMS` / `METHOD_NOT_FOUND` / `AUTH_ERROR` / `INTERNAL_ERROR`,**不新增错误码**。 --- ## 五、分页协议(核心) ### 5.1 为什么用游标而非页码 | 方案 | 问题 | |---|---| | 只返回前 N 条 + `truncated` 标记 | Agent 无法继续取,**数据缺失**(且无补救手段) | | `OFFSET n LIMIT m` 页码分页 | 翻页期间数据增删会导致**漏行或重复行**;深分页性能随 offset 线性退化 | | **游标(keyset)分页** | 按不可变排序键推进,**不重不漏**,深分页性能恒定 ✅ | ### 5.2 入参(通用) | 参数 | 默认 | 说明 | |---|---|---| | `page_size` | 20 | 聚合类上限 **200**,明细类上限 **100** | | `cursor` | — | 上轮返回的 `next_cursor`,首轮不传;与 `page` 互斥 | | `page` | — | 兼容用页码(内部转 OFFSET,**仅在数据不变时稳定**,不推荐) | | `include_total` | `false` | 置 `true` 时执行 count,受 `count_cap=50000` 限制 | ### 5.3 返回(`data.page_info`) ```json { "returned": 20, "page_size": 20, "has_more": true, "next_cursor": "", "sort_by": "outer_code,id", "page_no": 3, "total_count": null, "total_count_capped": false, "truncated_by_bytes": false } ``` ### 5.4 游标编码(无状态、可校验) `base64url({ "v":1, "t":"tool_name", "k":[排序键值...], "f":"filterHash", "p":pageNo })` - **`k`**:最后一行/组的排序键值(游标推进依据); - **`f`**:入参过滤条件 + **当前用户权限指纹**(授权仓库集合 / 供应商集合)的哈希。校验不一致直接报错"cursor 与当前过滤条件或权限不匹配,请从第一页重新开始",防止串用游标导致漏数; - **`p`**:页码,用于 `max_pages` 保护(聚合默认 20 页、明细默认 50 页,超限提示收窄条件); - 每页以 `limit+1` 探测 `has_more`,**不额外执行 count**; - 单页响应超 ~200KB 时自动下调 `page_size` 并置 `truncated_by_bytes=true`(换页而非丢数据)。 ### 5.5 各工具固定排序键 | 工具 | 排序键(`sort_by`) | |---|---| | `inventory_sn_trace` | 随入参:`product_sn,id` / `inner_code,id` / `outer_code,id` | | `inventory_stock_aggregate` | 随 `group_by`:`product_code` / `warehouse_id,product_code` / `inventory_status,product_code` / `time_bucket,product_code` | | `purchase_arrival_aggregate` | `purchase_no` / `vendor_id,purchase_no` / `product_code,purchase_no` / `time_bucket,purchase_no` | | `finance_balance_aggregate` | `order_code` / `partner_code,order_code` / `time_bucket,order_code` | | `warehouse_list` | `inner_code,id`(INNER) / `outer_code,id`(OUTER) / `outer_code,id`(DELIVERY) / **`delivery_code,id`(ORDER_DELIVERY)** / `order_code,id`(STOCK) / `product_sn,id`(SN) / **`order_code,version_code`(RECALL)** | | `purchase_list` | `purchase_no,id`(ORDER/ITEM) / `order_id,purchase_id`(ORDER_BIND) / `purchase_no,id`(HISTORY) | | `finance_list` | `,id` / `order_code,id`(CHARGE) | | **`master_data_list`** | 各自主键:`partner_code` / `customer_code` / `agent_code` / `vendor_code` / `product_code` / `user_id` / `warehouse_code` / `company_code` | | **`approval_list`(v13)** | `apply_time desc, id`(TODO)/ `approve_time desc, id`(DONE) | > 时间维度统一以 **`time_bucket`** 作为分组别名(`TIME_MONTH` → `2026-01`,`TIME_QUARTER` → `2026Q1`),实现上用**区间下推**而非列函数(见 14.4 技术注意)。 SQL 形态(MySQL 8 支持行构造器,但**为索引友好改用显式比较**): ```sql -- 单值过滤:纯索引游标(推荐路径) where outer_code = :oc and id > :lastId order by id limit :n+1; -- 多值 IN:跨多段索引区间,会产生 filesort(可接受代价,见 5.6) where product_code in (:list) order by product_code, id limit :n+1; ``` ### 5.6 Agent 侧循环指令(写入 `description`) > 分页查询工具。若返回 `page_info.has_more` 为 `true`,**必须**携带 `page_info.next_cursor` 再次调用本工具,重复直到 `has_more` 为 `false`,否则结果不完整。不要用 `page` 参数替代 `cursor`。 ### 5.7 已知局限(诚实声明) 1. **多值 IN + 游标会产生 filesort**:故明细类 `page_size ≤ 100`、IN 长度 ≤ 50,并优先引导 Agent 使用单号/SN 精确查询。 2. **不提供快照一致性**:keyset 保证不重不漏,但翻页期间新增数据可能出现在后续页。严格快照需 `and update_time <= :as_of`,而**全部表的时间列无索引**,代价过高,本版不做。 3. **聚合跨页口径**:聚合结果随上游数据变化,`page_info` 中的 `page_no` 仅代表游标推进次序,不代表固定快照。 > **补充**:全局统计**不应由 Agent 翻页累加**(会退化为多次全表扫描且可能被 `max_pages` 截断),应使用聚合工具的 `mode=SUMMARY` 一次算完,详见 **第十三章**。 --- ## 六、需要新增的索引 > **v15 实测更新**:在 `oms_test` 核对 `information_schema` 后发现 **P0-1 / P0-2 / P1-1 / P1-2 四枚索引已存在**(此前巡检查漏),无需再建;仅 **P1-3 覆盖索引**缺失,已在 `oms_test` 执行并实测:建索引 1.3s、按产品聚合 **141.8ms → 66.6ms**、`EXPLAIN` 显示 `Using index`(免回表)。生产库执行前请用 [mcp-tools-index-ddl.sql](./mcp-tools-index-ddl.sql) 的核对语句确认。 ### 6.1 必加(P0) | 序号 | 表 | 索引名 | 列 | 服务的工具 | 理由 | |---|---|---|---|---|---| | P0-1 | `oms_inventory_outer` | `idx_outer_code` | `outer_code` | `inventory_flow` | 出库单号是该工具主入口,当前**仅 `order_code` 有索引**,按单号查会全表扫描 | | P0-2 | `oms_inventory_outer_detail` | `idx_outer_code` | `outer_code` | `inventory_flow` | 明细批量查询依赖它;当前**仅主键**,不补索引则新增的 `listByOuterCodeList` 反而成为性能陷阱 | ```sql ALTER TABLE oms_inventory_outer ADD INDEX idx_outer_code (outer_code), ALGORITHM=INPLACE, LOCK=NONE; ALTER TABLE oms_inventory_outer_detail ADD INDEX idx_outer_code (outer_code), ALGORITHM=INPLACE, LOCK=NONE; ``` ### 6.2 建议加(P1,防数据增长后退化) | 序号 | 表 | 索引名 | 列 | 理由 | |---|---|---|---|---| | P1-1 | `oms_inventory_inner` | `idx_order_code` | `order_code` | `inventory_flow` 以 `order_code` 为入口时走 `selectOmsInventoryInnerByOrderCodeList`;该列**当前无索引** | | P1-2 | `oms_purchase_order` | `idx_vendor_id` | `vendor_id` | 采购聚合/明细若要按供应商收窄;当前仅 `purchase_no`(UK) | | P1-3 | `oms_inventory_info` | `idx_pc_status_amt` | `(product_code, inventory_status, inner_price, outer_price)` | **覆盖索引**:聚合可完全走索引、免回表(实测 count 14.2ms → 含 sum 需回表 36.7ms)。**仅对这张线性增长的大表加**,小表不值得(应收表 627 行全表聚合仅 21.3ms)。需权衡索引体积与入库写入放大 | ```sql -- P1-3 覆盖索引(需权衡写入开销,建议 DBA 评估后执行) ALTER TABLE oms_inventory_info ADD INDEX idx_pc_status_amt (product_code, inventory_status, inner_price, outer_price), ALGORITHM=INPLACE, LOCK=NONE; ``` > **注意**:加 P1-3 前须确认字段长度不超 InnoDB 索引上限(`product_code`/`inventory_status` 为 `varchar(255)` utf8mb4,四列合计约 2.1KB < 3072B,可行)。 ```sql ALTER TABLE oms_inventory_inner ADD INDEX idx_order_code (order_code), ALGORITHM=INPLACE, LOCK=NONE; ALTER TABLE oms_purchase_order ADD INDEX idx_vendor_id (vendor_id), ALGORITHM=INPLACE, LOCK=NONE; ``` ### 6.3 可选(P2,仅在启用对应能力时添加) | 序号 | 表 | 索引名 | 列 | 启用条件 | |---|---|---|---|---| | P2-1 | `oms_purchase_order_map` | `idx_order_id` | `order_id` | 若要支持"按订单号反查采购单"(当前**不支持**该入口) | | P2-2 | `oms_purchase_order_map` | `idx_purchase_id` | `purchase_id` | 同上 | | P2-3 | ~~`oms_inventory_info.idx_warehouse_id`~~ | — | — | **已被 P2-7 取代**(`(warehouse_id, inventory_status)` 组合更适用) | | P2-4 | ~~`oms_stock_info.idx_order_code`~~ | — | — | **已并入 P2-8** | | P2-5 | `oms_receivable_receipt_detail` | `idx_write_off_id` | `write_off_id` | 若要支持按核销单号反查(当前**不支持**) | | P2-6 | `oms_payable_payment_detail` | `idx_write_off_id` | `write_off_id` | 同上 | | P2-7 | `oms_inventory_info` | `idx_wh_status` | `(warehouse_id, inventory_status)` | 若要支持 `warehouse_list(entity=SN)` **仅按仓库+状态**查询(大表,无此索引必全表扫描) | | P2-8 | `oms_stock_info` | `idx_order_code` | `order_code` | 备货范围查询(`warehouse_list(entity=STOCK)`、`inventory_flow.stock`);当前 526 行 | | P2-9 | `oms_finance_charge` | `idx_charge_status` | `charge_status` | 计收状态维度统计;点查已可用 `order_code`(UK) | | P2-10 | `oms_purchase_order` | `idx_status_date` | `(status, purchase_date)` | 采购范围查询与时间维度;当前 941 行 | | P2-11 | `oms_receivable_bill` / `oms_payable_bill` / `oms_receipt_bill` / `oms_payment_bill` / `oms_invoice_bill` / `oms_ticket_bill` | `idx_partner_code` / `idx_vendor_code` | `partner_code` / `vendor_code` | `finance_balance_aggregate(group_by=PARTNER)` 与按合作伙伴列表查询;当前均 < 700 行 | | P2-12 | `oms_inventory_info` | `idx_create_time` | `create_time` | `group_by=TIME_MONTH/TIME_QUARTER` 时间维度(**大表**);小表无需 | | P2-13 | `oms_inventory_info` | `idx_wh_pc` | `(warehouse_id, product_code)` | `group_by=PRODUCT_WAREHOUSE` 的 **LIST** 模式游标对齐(否则 filesort,见 15.2) | | P2-14 | `oms_inventory_info` | `idx_status_pc` | `(inventory_status, product_code)` | `group_by=STATUS` 的 **LIST** 模式游标对齐 | | P2-15 | `oms_inventory_inner` | `idx_purchase_no` | `purchase_no` | `metrics=ARRIVAL_DELAY_DAYS` 需按采购单号 join,当前无索引(见 15.3) | | P2-16 | `order_delivery` | `idx_status_created` | `(delivery_status, created_at)` | manage 域发货单按状态/时间范围查询;当前 355 行 | | P2-17 | `delivery_list` | `idx_delivery_deleted` | `(delivery_id, deleted_at)` | manage 域发货 SN 明细的软删除过滤(现只能靠 `idx_delivery_id` 后再过滤);当前 32,718 行 | | P2-18 | `project_order_info_recall` | `idx_order_code` | `order_code` | 撤回历史按合同号查询;当前仅主键(40 行) | | P2-19 | ~~`oms_inventory_inner_detail`~~ | — | — | **不采用**:该表实测仅 1 行(未启用),入库明细以 `oms_inventory_info`(按 `inner_code`)为准,故无需索引 | | P2-20 | `product_info` | `idx_vendor_code` | `vendor_code` | `master_data_list(entity=PRODUCT)` 按制造商过滤;当前 341 行 | | P2-21 | `project_poc_info` | `idx_project_id` | `project_id` | `project_list(entity=POC)` 按项目查询;当前**仅主键**(998 行) | | P2-22 | `oms_quotation` / `oms_quotation_product_info` | `idx_quotation_code` / `idx_quotation_id` | `quotation_code` / `quotation_id` | `project_list(entity=QUOTATION)`;当前均**仅主键**(0 行,启用后加) | | P2-23 | `project_product_info` | `idx_product_bom_code` | `product_bom_code` | `cross_domain_aggregate` 的 `PRODUCT` 维度与"跨项目按产品查询";当前仅 `idx_project_id` | | P2-24 | `project_info` | `idx_partner_code` | `partner_code` | `cross_domain_aggregate` 的 `PARTNER` 维度(`project_info` 现有 `customer_code`/`agent_code` 索引,缺 `partner_code`) | | P2-25 | `project_order_info` | `idx_approve_time` | `approve_time` | `cross_domain_aggregate` 的 `MONTH`/`QUARTER` 时间维度(`order_code`/`partner_code`/`project_id` 已有索引) | | P2-26 | `bu_todo_completed` | `idx_approve_user_time` | `(approve_user, approve_time)` | `approval_list(entity=DONE)` 按"审批人 + 时间"翻页;当前 5,876 行仅 `idx_business_key`,全表扫尚可,**增长后需补** | > 以上 P2 的触发条件统一为:**对应表行数增长到 10 万+**(或该查询已成为高频热点)。当前除 `oms_inventory_info`、`oms_inventory_delivery_detail` 外全部 < 1000 行,**暂不执行**。 ### 6.4 明确"不能加"的索引 | 表.列 | 原因 | |---|---| | `oms_inventory_delivery.order_code` | **该列不存在**(由 `oms_inventory_outer` join 得出),建索引会直接失败;按订单号查发货必须经 `outer_code` 关联 | | `oms_inventory_outer.warehouse_id` | **该列不存在**(仓库在 `oms_inventory_outer_detail` 上) | > **时间列索引策略(v7 调整)**:v5 曾"撤销时间范围过滤",因其无索引会全表扫描。v7 依据**表规模分级**(14.4)改为:**小表**(<1000 行)允许时间范围过滤(全表扫描 <10ms,无需索引);**大表**(`oms_inventory_info` 等)的时间维度聚合需先加 `create_time` 索引(P2-12),或仅接受"带索引键收窄后 + 时间二次过滤"。 ### 6.5 索引上线注意 1. 先在生产/正式测试库执行 `SHOW INDEX FROM ` 复核本方案的"现有索引"结论(本文结论基于 `oms_test`); 2. 使用 `ALGORITHM=INPLACE, LOCK=NONE` 在线加索引,建议低峰执行; 3. 加完用 `EXPLAIN` 验证目标语句 `type` 非 `ALL`; 4. 加索引属 DDL,**不改字段、不改业务逻辑**。 --- ## 七、需要新增的只读 SQL | # | 位置 | 内容 | 目的 | |---|---|---|---| | 1 | `InventoryOuterDetailMapper.java/.xml` | `listByOuterCodeList(List)`:`outer_code in (...)` | 消除出库明细 N+1(依赖 P0-2 索引) | | 2 | `OmsInventoryDeliveryDetailMapper.java/.xml` | `listByDeliveryIdList(List)`:`delivery_id in (...)` | 消除发货明细 N+1(`idx_delivery_id` 已具备) | | 3 | `InventoryInfoMapper.java/.xml` | `aggregateStock(List productCodes, String groupBy, String lastKey, int limit, ...)`:按 `group_by` 动态分组 + 游标 + `order by` | `inventory_stock_aggregate` 的 SUMMARY/LIST 两种模式 | | 4 | `InventoryInfoMapper.java/.xml` | `aggregateStockByWarehouse(List productCodes)`:`group by product_code, warehouse_id` | 上者的组内仓库拆分 | | 5 | `OmsPurchaseOrderMapper.java/.xml` | `aggregateArrival(String groupBy, String lastKey, int limit, ...)`:`oms_purchase_order` ⋈ `item` 动态分组聚合 | `purchase_arrival_aggregate` | | 6 | `OmsReceivableBillMapper.java/.xml` + `OmsPayableBillMapper.java/.xml` | 按 `order_code` / `partner_code` / `status` / `time_bucket` 动态分组的余额聚合 | `finance_balance_aggregate` | | 7 | `OmsStockInfoMapper.java/.xml` | 新增 `list(OmsStockInfo)` 带 `order_code` / `stock_status` / `create_time` 范围条件(**当前仅 `queryAll` 且无索引条件**) | `warehouse_list(entity=STOCK)`、`inventory_flow.stock` | | 8 | `OmsFinanceChargeMapper.java/.xml` | 复用现有 `selectOmsFinanceChargeList`;如需按计收状态/时间聚合,追加 `` 条件 | `finance_list(entity=CHARGE)` 与 `finance_order_position.charge` | | 9 | `OmsPurchaseOrderHistoryMapper.java/.xml` | 新增按 `purchase_no` / 时间范围查询历史(现仅按 `purchase_id` 单值) | `purchase_list(entity=HISTORY)` | | 10 | `VendorInfoMapper.java/.xml` | 复用 `selectVendorInfoList`(支持 `vendorCodeList` / `vendorNameList` 批量) | `master_data_list(entity=VENDOR)` | | 11 | `InventoryOuterMapper.xml` / `OmsInventoryInnerMapper.xml` / `InventoryDeliveryMapper.xml` / 财务各表 | 追加**时间范围与状态**的 `` 条件(**仅小表启用**,见 6.4 时间列索引策略) | 三域列表工具的范围过滤 | | 12 | `OrderDeliveryMapper.java/.xml` | 新增按 `code_list` / `status_list` / 时间范围查询,并强制 `deleted_at is null` | `warehouse_list(entity=ORDER_DELIVERY)`(manage 域发货单,含签收) | | 13 | `DeliveryListMapper.java/.xml` | 新增按 `delivery_id in (...)` 批量查 SN,带 `deleted_at is null` | 同上的 SN 明细 | | 14 | `OmsInventoryInnerDetailMapper.java/.xml` | 新增按 `inner_code in (...)` 查询 | `warehouse_list(entity=INNER, include_detail=true)` | | 15 | 主数据 Mapper(`PartnerInfoMapper` / `CustomerInfoMapper` / `AgentInfoMapper` / `ProductInfoMapper` / `VendorInfoMapper` / `SysUserMapper`) | 复用现有 `selectXxxList`;为 `PRODUCT` 增加 `productCodeList` 批量条件 | `master_data_list`(含"批量编码翻译"能力) | | 16 | `ProjectOrderInfoRecallMapper.java/.xml` | 新增按 `order_code` 查询 | `warehouse_list(entity=RECALL)` 撤回历史 | | 17 | `ProjectInfoMapper` / `ProjectProductInfoMapper` / `ProjectWorkProgressMapper` / `ProjectPocInfoMapper` / `QuotationMapper`(+`QuotationProductInfoMapper`) | 新增/复用 list 查询,补 `project_id in (...)` / `code_list` / 时间范围条件 | `project_list`(POC/报价表当前仅主键,需配套 P2-21/P2-22) | | 18 | **新增 `CrossDomainAggregateMapper.java/.xml`** | 按**链路**各一个分组聚合 select:`SALES` / `PURCHASE` / `STOCK` / `FINANCE_AR` / `FINANCE_AP`(维度动态、时间区间下推) | `cross_domain_aggregate` | | 19 | 各聚合 Mapper | 为时间维度追加**区间下推**条件(`>= begin and < end`),禁止 `date_format()` | 所有聚合与透视工具的时间维度 | | 20 | `SysUserMapper` | 复用按 `user_id in (...)` / 部门查询 | `master_data_list(entity=USER)` | | 21 | `OrderInfoMapper.java/.xml` | 新增按 `order_code in (...)`(**`trim()` 对齐**)/ `order_type` / `status` / 时间范围查询,**默认 `deleted_at is null`** | `project_list(entity=CONTRACT)`;并供 `warehouse_list(entity=ORDER_DELIVERY)` 补全合同信息 | | 22 | `OrderListMapper.java/.xml` | 新增按 `order_id in (...)` 批量查询,**默认 `deleted_at is null`** | `project_list(entity=CONTRACT_PRODUCT)` | | 23 | **新增 `BuTodoMapper.java/.xml`** | `listTodo(approveUser, processKeyList, businessKey, timeRange)` 与 `listDone(...)`:分别查 `bu_todo` / `bu_todo_completed`,按 `apply_time` / `approve_time` 倒序 + 游标分页 | `approval_list(entity=TODO/DONE)` | | 24 | `OmsFinAttachmentMapper.java/.xml` | 新增按 `related_bill_id in (...)` + `related_bill_type` 查询,**过滤 `del_flag='0'`**(只返回元数据列,不含 `file_path` 内容) | `finance_list(entity=ATTACHMENT)` | | 25 | `OmsReceivableBillMapper` + `OmsReceivableReceiptDetailMapper` | 新增**时点重算**聚合:`Σ应收(create_time ≤ T)` 与 `Σ已收(receipt_time ≤ T)`(支持 `as_of_date`) | `finance_balance_aggregate(as_of_date=…)` 的历史时点余额 | > 其余查询全部复用现有 `in (...)` 批量方法,不新增。 --- ## 八、权限对齐规则 现有权限是**异构**的,必须逐个工具对齐其对应页面入口。 ### 8.1 行级权限(数据范围) | 数据域 | 机制 | 实现要求 | |---|---|---| | 仓储(出入库/发货/库存/备货/SN) | `IInventoryAuthService`:`authAll()` / `currentVendor()` / `authWarehouse()` / `authProductCode()`;部分在 Controller 拼、部分在 Service 拼;发货另带 `@DataScope("t8")` | 工具内**按对应 Controller 的拼法**回填。**不要照抄** `OmsInventoryInnerServiceImpl` 只取 `currentVendor().get(0)` 第一家的既有缺陷 | | 采购(采购单/明细/历史/供应商/绑定) | 行级 `authVendorCodeList`(Controller 层由 `currentVendor()` 生成) | `purchase_list(entity=ORDER/ITEM/HISTORY)` 回填 `authVendorCodeList`;`entity=VENDOR` 直接用 `currentVendor()` 结果集 | | 财务 | **无行级权限**,仅菜单权限 | 只做菜单校验 | | 订单(对比参考) | `selectProjectOrderInfoList` 自带 `@DataScope("t5")` + `authSql` | 走 service 即自动生效 | ### 8.2 菜单权限(`isPermitted`)—— 逐个工具/entity 对照来源 | 工具 / entity | 权限串来源(实现时从该 Controller 的 `@RequiresPermissions` 抄取) | 已知值 | |---|---|---| | `inventory_sn_trace` / `inventory_stock_aggregate` | `InventoryInfoController`、`VueInventoryInfoController` | 待抄取 | | `inventory_flow` | `InventoryOuterController`、`VueDeliveryController` | 待抄取 | | `warehouse_list`(`INNER`/`OUTER`/`DELIVERY`/`STOCK`/`SN`) | `OmsInventoryInnerController`、`InventoryOuterController`、`VueDeliveryController`、`OmsStockInfoController`、`InventoryInfoController` | 待抄取 | | `warehouse_list`(`ORDER_DELIVERY`) | manage 域发货单对应 Controller(`OrderDeliveryController` 及 `VueDeliveryController` 同路径) | 待抄取 | | `warehouse_list`(`RECALL`) | `ProjectOrderInfoController`(撤回/版本相关接口) | 待抄取 | | `purchase_arrival_aggregate` / `purchase_list`(`ORDER`/`ITEM`/`ORDER_BIND`) | `OmsPurchaseOrderController` | **`sip:purchaseorder:list`** ✅ | | `purchase_list`(`HISTORY`) | `OmsPurchaseOrderController` 历史接口 | 待抄取 | | `master_data_list`(`PARTNER` / `CUSTOMER` / `AGENT`) | `PartnerInfoController` / `CustomerInfoController` / `AgentInfoController`(含 `Vue*` 版本) | 待抄取 | | `master_data_list`(`VENDOR`) | `VendorInfoController`、`VueVendorInfoController` | 待抄取 | | `master_data_list`(`PRODUCT`) | `ProductInfoController`、`VueProductInfoController` | 待抄取 | | `master_data_list`(`USER`) | `SysUserController`(系统用户) | 待抄取 | | `master_data_list`(`WAREHOUSE`) | `OmsWarehouseInfoController` | 待抄取 | | `master_data_list`(`COMPANY`) | `OmsCompanyInfoController` | 待抄取 | | `project_list`(`PROJECT`/`PROJECT_PRODUCT`/`PROGRESS`/`POC`) | `ProjectInfoController`、`ProjectOrderInfoController`(含 `Vue*` 版本) | 待抄取 | | `project_list`(`QUOTATION`) | `QuotationController`(含 `Vue*` 版本) | 待抄取 | | `project_list`(`CONTRACT`/`CONTRACT_PRODUCT`) | manage 域合同对应 Controller(`OrderInfoController` / `VueOrderInfoController` 等) | 待抄取 | | `cross_domain_aggregate` | **按链路做最小权限校验**:需同时具备所访问链路的菜单权限(`SALES` 需订单/项目查看权限;`FINANCE_AR`/`FINANCE_AP` 需对应财务权限) | 待抄取 | | `finance_order_position` / `finance_balance_aggregate` | `OmsReceivableBillController`、`OmsPayableBillController` | **`finance:receivable:list`** ✅,应付待抄取 | | `finance_list`(`RECEIPT`/`PAYMENT`/`INVOICE`/`TICKET`) | `OmsReceiptBillController`、`OmsPaymentBillController`、`OmsInvoiceBillController`、`OmsTicketBillController` | 待抄取 | | `finance_list`(`CHARGE`) | `OmsFinanceChargeController` | 待抄取 | | `finance_list`(`ATTACHMENT`) | `OmsFinAttachmentController`(或财务单据 Controller 的附件接口) | 待抄取 | | `approval_list`(`TODO`/`DONE`) | 待办/已办接口对应 Controller(`OmsPurchaseOrderController` 的 approveList/approvedList、财务付款审批接口、`ApprovalTaskController` 等) | 待抄取 | ### 8.3 统一要求 1. `handle` 第一行做 `isPermitted` 校验,不通过返回 **`AUTH_ERROR`**(**不要返回空列表**,否则 Agent 会误判为"无数据"); 2. 行级权限指纹并入游标 `filterHash`(见 5.4),避免权限变化导致游标错位; 3. "权限不足"与"确实无数据"必须在响应中可区分:前者走 `error`,后者 `data.items = []`。 --- ## 九、聚合口径定义 | 工具 | 度量 | 口径 | |---|---|---| | `inventory_stock_aggregate` | `in_stock_qty` / `out_stock_qty` | `count(*) where inventory_status='0'` / `='1'` | | | `inner_amount` / `outer_amount` | `sum(inner_price)` / `sum(outer_price)`,**NULL 计 0** | | | 未税金额(如返回) | 按明细税率换算;**`tax_rate` 为空时按 0 计**(实测存在 NULL) | | `purchase_arrival_aggregate` | `purchase_qty` / `inner_qty` / `pending_qty` | `oms_purchase_order_item.quantity` / `inner_quantity` / 两者差值 | | | `arrival_rate` | `inner_qty / purchase_qty`,HALF_UP 保留 2 位;分母为 0 时返回 0 | | | `amount_total` / `tax_total` | `sum(amount_total)` / `sum(tax_total)` | | `finance_balance_aggregate` | 应收侧 4 值 / 应付侧 4 值 | 直接 `sum` 冗余列:`unreceived_amount`、`uninvoiced_amount`、`unpaid_payment_amount`、`unreceived_ticket_amount`。**不重算**,避免口径偏差与额外开销 | **v7 新增度量口径** | 工具 | 度量 | 口径 | |---|---|---| | `purchase_arrival_aggregate` | `arrival_delay_days` | `datediff(入库时间, item.delivery_date)` 的均值;未入库的不计(或按参数 `include_pending_delay=true` 用今天计算);分母 0 返回 null | | `finance_balance_aggregate` | `overdue_days` | `datediff(今天, plan_receipt_date)`(仅当 `unreceived_amount > 0`);`< 0` 返回 0 表示未到期 | | 财务类 | `PARTNER` 维度 | 应收按 `partner_code`(客户),应付按 `vendor_code`(制造商);两者需在 `metadata` 注明维度主体差异 | **v7 新增维度口径** | `group_by` | 分组键 | 说明 | |---|---|---| | `STATUS` | 各表状态列 | 返回状态编码 + `*Name`(枚举翻译) | | `TIME_MONTH` | `time_bucket = yyyy-MM` | **区间下推**实现:`create_time >= '2026-01-01' and create_time < '2026-02-01'`,禁止 `date_format(create_time,...)` | | `TIME_QUARTER` | `time_bucket = yyyyQn` | 同上,按季度区间 | | `PARTNER` | `partner_code` / `vendor_code` | 见上表维度主体差异 | | `VENDOR` | `vendor_id` | 采购侧制造商(依赖 P1-2) | | `PRODUCT` | `product_code` | 采购侧按产品 | > 时间维度的默认区间:未传 `time_range` 时取**近 12 个月**,避免无界扫描;区间跨度上限 36 个月。 **v13 新增口径** | 工具 | 项 | 口径 | |---|---|---| | `finance_balance_aggregate` | `group_by=OVERDUE_BUCKET`(账龄分桶) | 基于 `datediff(今天, plan_receipt_date)` 分桶:**`0-30` / `31-60` / `61-90` / `90+`**;**仅统计 `unreceived_amount > 0` 的行**;边界左闭右闭(`=30` 入 `0-30`);同时返回 `in_bucket_qty`、`in_bucket_amount` | | `finance_balance_aggregate` | `as_of_date`(历史时点余额) | **重算口径**:`应收(≤T) = Σ receivable_bill.total_price_with_tax where create_time ≤ T`;`已收(≤T) = Σ receipt_detail.receipt_amount where receipt_time ≤ T`;时点未收 = 两者差。**与冗余列"当前值"口径不同**,`metadata` 必须标注 `basis: RECALCULATED@`,且未传 `as_of_date` 时仍用冗余列(`basis: CURRENT`) | 上述口径必须写入返回的 `metadata.aggregation_rule`。 --- ## 十、实施步骤 1. **索引 DDL**:执行 P0(必须)+ P1(建议),并按 **16.4 的执行方案**(预检 → 分步 → 验证 → 可回滚)推进;P2 暂不执行(按表规模触发); 2. **新增只读 SQL**:第七节 25 项; 3. **公共能力**:在 `llm/tools/support` 增加分页/游标/入参解析/权限回填的轻量基类(继承 `AbstractMcpToolProvider`),含 `page_size` 上限、`limit+1` 探测、游标编解码与 `filterHash` 校验、`max_pages` 保护、`mode`/`group_by`/`metrics`/`entity`/`dimensions` 枚举校验与**类型规范化**(varchar 强制字符串)、**参数冲突校验**(见 15.5)、**表白名单**(见 15.12)、**从库路由开关**(见 16.6)、**时间区间配置**(见 16.5); 4. **实现 13 个工具**:A 类 3 个(含 `stock`/`charge` 分组)→ B 类 3 个(SUMMARY 优先,再补 LIST;含 `OVERDUE_BUCKET` 与 `as_of_date`)→ C 类 5 个(`entity` 参数化,含 `master_data_list` 与 **`approval_list`**)→ D 类 2 个(`project_list`、`cross_domain_aggregate`),继承基类 + `@Component` 自动注册(无需改 `ToolInitializer`); 5. **RAG 路由(16.7)与 D 类同期上线**:`ToolRetriever` / `ToolRouter` + `tools/list` 的 `query`/`detail` 支持,否则 13 个工具的 schema token 不可接受; 6. **逐层验收**:每完成一类即按第十一节验证,并记录 P95 延迟、返回字节数、schema token 三个指标。 ### 10.1 建议的实施批次(可按需截断) | 批次 | 内容 | 价值 | |---|---|---| | 第 1 批 | 索引 P0 + 只读 SQL 1/2 + 公共基类 + A 类 3 个工具 | 覆盖"单号追溯"与"订单财务/货流全景" | | 第 2 批 | B 类 3 个(含 SUMMARY/Top-N/时间维度) | 覆盖"统计分析",避免 Agent 翻页 | | 第 3 批 | C 类 5 个(列表/范围查询 + 主数据批量翻译 + **审批待办/已办**) | 补齐"本月有哪些/多少""编码→名称""我还有哪些单要审" | | 第 4 批 | **RAG 路由(16.7)** | 控制 schema token、降低选错率 | | 第 5 批 | **D 类 2 个(项目/POC/报价、受限跨域透视)** | 补齐项目域与组合分析 | | 第 6 批 | P1 索引(含覆盖索引)+ 性能调优 + 压测(见 15.13) | 大规模下提速与容量验证 | --- ## 十一、验收标准 ### 11.1 正确性 1. **不重不漏**:同一过滤条件下,逐页拉完的结果集与一次性全量取的结果**逐行比对一致**; 2. **翻页稳定性**:翻页过程中并发插入/删除若干行,结果集不出现重复行; 3. **游标防误用**:篡改 cursor 中的过滤条件/权限指纹 → 必须报错,而非返回错数据; 4. **口径一致**:聚合结果与页面同条件展示一致(金额、数量、税率为 NULL 的兜底); 5. **端到端**:以真实单号(发货 775、出库/采购/应收各一条)跑完整翻页,`has_more=false` 后条目数与页面一致。 ### 11.2 性能 6. 每个游标过滤键 `EXPLAIN` 结果 `type` 非 `ALL`; 7. `inventory_sn_trace` 在 50 个 SN 下目标 **P95 < 300ms**; 8. 记录各工具 P95 延迟与返回字节数(作为基线入库)。 ### 11.3 保护机制 9. `max_pages` 超限、`include_total` 超 `count_cap`、单页字节超限,三条路径均触发预期行为。 ### 11.4 安全 10. 无权限用户调用 → 返回 `AUTH_ERROR`(非空列表); 11. 供应商/仓库行级权限生效:越权数据不可见。 --- ## 十二、遗留与待确认 | # | 事项 | 说明 | |---|---|---| | 1 | 生产库量级 | 本文量级来自 `oms_test`(`oms_inventory_info` 6.5 万)。若生产为百万级,保持 `product_sn_list ≤ 50`、禁止仅按 `product_code`/`warehouse_id` 查 SN,并触发 P2-7/P2-12 索引 | | 2 | 生产索引复核 | 执行 `SHOW INDEX FROM
` 复核 2.5 节结论(索引结构理论上与测试库一致,但需确认) | | 3 | ~~聚合分页粒度~~ | **已定**:库存聚合以**产品**为页、仓库作组内嵌套;采购按 `purchase_no`;财务按 `order_code`。按"产品+仓库"行粒度分页需重新评估跨索引排序成本 | | 4 | `include_total` 默认值 | 当前默认 `false`(避免百万级 count);若业务更关心总数可改为 `true` + 提高 `count_cap` | | 5 | 菜单权限串 | 财务/采购部分工具与 entity 的精确 `@RequiresPermissions` 需实现时逐个从 Controller 抄取(对照表见 8.2) | | 6 | P0/P1 索引 DDL 执行窗口 | 需 DBA 在低峰期执行,P1-3 覆盖索引须先评估写入放大 | | 7 | 时间维度默认区间 | 当前定义为**近 12 个月**、跨度上限 36 个月;若业务需要更长历史,需同时补大表时间索引(P2-12)并放宽上限 | | 8 | 从库路由(可选) | `slave` 数据源当前 `enabled=false`;若要启用统计查询走从库,需 DBA 确认延迟与可用性 | | 9 | **"在库"口径** | **未配置化(2026-09-23 回滚)**:曾尝试以 `mcp.inventory.stock-basis` 配置化,经确认**不引入配置机制**,已全部回滚。当前口径写死在 [InventoryInfoMapper.xml](../ruoyi-sip/src/main/resources/mapper/inventory/InventoryInfoMapper.xml) 的 `inventory_status='0'`,并在工具 `metadata` 中声明"**未扣除已发货占用**,业务待确认";业务确认后需改代码 | | 10 | **价格含税口径** | **未配置化(2026-09-23 回滚)**:同上,不引入配置。当前仅在 `inventory_stock_aggregate` / `inventory_sn_trace` 的 `metadata` 中声明"是否含税未经业务确认,不做换算",工具不做任何含税/未税换算 | | 11 | **备份表白名单** | 库中存在 `oms_inventory_info_copy1`(42,836 行) 等大量备份表,实现时必须以**白名单**登记可访问表(见 15.12) | | 12 | ~~项目进度 / POC / 报价~~ | **已纳入(见 16.1)**:`project_list` 工具,entity = `PROJECT`/`PROJECT_PRODUCT`/`PROGRESS`/`POC`/`QUOTATION`/**`CONTRACT`**/**`CONTRACT_PRODUCT`** | | 13 | **RAG 工具路由** | 13 个工具下**必须实现**(见 16.7),否则每轮 schema token 不可接受;需 20 条中文问题做命中率验收 | | 14 | **跨域透视成本** | `cross_domain_aggregate` 的 `SALES` 链路为 3 表 join + 分组,属**兜底能力**;须在生产数据上验证 P95,必要时补 P2-23/24/25 | | 15 | **两套订单模型的关系** | manage 域 `order_info`(366) 与项目域 `project_order_info`(809) **仅 330/366 可按 `order_code` 对齐**;二者业务关系(新旧两代?两套视角?)**需业务确认**,并确认"合同编号"以哪张表为准 | | 16 | **`order_type` 取值含义** | 实测为 `zq`/`dls`(非注释的 1/2),需业务确认 `zq`=直签、`dls`=代理商 | | 17 | ~~是否纳入审批待办/已办~~ | **已纳入(v13)**:新增工具 `approval_list`(entity = `TODO`/`DONE`),工具数 12 → 13;数据源 `bu_todo`/`bu_todo_completed`,**无需碰 Flowable `act_*`**(见 A.15) | | 18 | ~~是否纳入账龄分桶~~ | **已纳入(v13)**:`finance_balance_aggregate` 增加 `group_by=OVERDUE_BUCKET`(`0-30`/`31-60`/`61-90`/`90+`),零成本(见 9 章 v13 新增口径) | | 19 | ~~是否需要财务历史时点余额~~ | **已纳入(v13)**:`finance_balance_aggregate` 增加 `as_of_date`,按明细重算时点余额;**口径与冗余列不同**,`metadata.basis` 标注 `RECALCULATED@` vs `CURRENT` | | 20 | **`fianance_ticket` 拼写** | `process_key` 存在源码级拼写错误(少一个 `n`),匹配必须按原样,勿"修正" | --- ## 十三、分析统计场景优化(v5,实测驱动) ### 13.1 问题:分析统计会退化为"Agent 驱动的多次全表扫描" 原设计只提供"游标分页明细",没有"一次算完"的能力。当 Agent 需要全局统计(如"某产品总在库量""所有订单未收款合计")时,只能翻页累加,后果: 1. **扫描量一点没省**:实测分页第 1 页 **34.4ms** ≈ 全局汇总 **38.1ms** —— 分页对聚合并不减少扫描; 2. **往返与 token 放大 N 倍**:5 页 = 5 次请求 + 5 份 schema; 3. **可能拿不到全量**:明细模式 6.5 万行 ≈ **648 页**(page_size=100),必被 `max_pages` 截断 → **与"数据不缺失"目标直接冲突**; 4. **回表成本**:`count(*)` 走覆盖索引 **14.2ms**,含 `sum(inner_price/outer_price)` 后 **36.7ms**(2.6×),因现有索引不含金额列。 ### 13.2 优化 1(收益最大,零 DDL):聚合工具新增 `mode=SUMMARY` 聚合类 3 个工具(#4/#5/#6)新增参数: | 参数 | 取值 | 说明 | |---|---|---| | `mode` | `SUMMARY`(默认)/ `LIST` | SUMMARY=一次算完并返回汇总,**不分页**;LIST=游标分页返回完整分组明细 | | `group_by` | 库存:`NONE` / `PRODUCT` / `WAREHOUSE` / `PRODUCT_WAREHOUSE` / `STATUS` / `TIME_MONTH` / `TIME_QUARTER`;采购:`NONE` / `ORDER` / `VENDOR` / `PRODUCT` / `STATUS` / `TIME_MONTH`;财务:`NONE` / `ORDER` / `PARTNER` / `STATUS` / `TIME_MONTH` | 分析维度;`NONE` 只返回一行总计。口径见第九章 | | `top_n` | int,默认 10,上限 100 | SUMMARY 下按度量取前 N,由 DB 完成(`order by desc limit N`) | | `metrics` | 数组,如 `["QTY","AMOUNT","ARRIVAL_RATE","ARRIVAL_DELAY_DAYS","OVERDUE_DAYS"]` | 只计算需要的度量,减少回表列 | | `include_summary` | bool | LIST 模式下附带一次全局总计(1 行),便于同时拿到"总量 + 明细" | **SUMMARY 实现**:单条 SQL 完成全量聚合: ```sql -- group_by=NONE:1 行总计(实测约 38ms @6.5 万行) select count(*) total_qty, sum(case when inventory_status = '0' then 1 else 0 end) in_stock_qty, sum(inner_price) inner_amount from oms_inventory_info where <过滤条件>; -- group_by=PRODUCT + top_n=10:排序取前 10 由 DB 完成 select product_code, count(*) total_qty, sum(case when inventory_status = '0' then 1 else 0 end) in_stock_qty, sum(inner_price) inner_amount from oms_inventory_info where <过滤条件> group by product_code order by total_qty desc limit 10; ``` **收益**:把"N 页 × 全表扫描"变为"**1 次全表扫描**",结果完整且不受 `max_pages` 影响。 ### 13.3 优化 2(零 DDL):Top-N 取代全量明细 分析统计的绝大多数问题是"最大的 / 最差的 / 占比 Top-N",用 `top_n` + 排序交给 MySQL,只返回 N 行。只有当需求明确是"逐行导出 / 对账"时才用 `LIST` 翻页。 **护栏**:`LIST` 模式对聚合类工具**必须给出范围**(如 `product_code_list`、`order_code_list`),否则返回 `INVALID_PARAMS` 并提示改用 `SUMMARY`,避免 Agent 无意识触发 648 页翻页。 ### 13.4 优化 3(需确认):覆盖索引,让"1 次全表扫描"也变快 - 现状:`idx_product_code` 只能覆盖 `count(*)`(实测 `Using index`,14.2ms);`sum(inner_price/outer_price)` 需回表(36.7ms)。 - 建议:**仅对 `oms_inventory_info` 加覆盖索引** `(product_code, inventory_status, inner_price, outer_price)`(见索引 **P1-3**)。 - 原则:**只对线性增长的大表加**;小表不加(应收表 627 行全表聚合仅 21.3ms,加索引反而增加写入开销)。 - 代价:索引体积 + 入库写 SN 时的写放大,需 DBA 评估。 ### 13.5 优化 4(零 DDL):数据类型严格对齐,防止索引退化 实测对比: | 写法 | EXPLAIN | 结果 | |---|---|---| | `where inner_code = 'R-20250917001'` | `key=idx_code`,`rows=const` | 精确定位 ✅ | | `where inner_code = 0`(数字) | `key=idx_code`,`rows=None`,`Extra=Using where` | **索引退化为逐行过滤** ❌ | | `where product_code = '9801H0BC'` | `rows=const` | 精确定位 ✅ | | `where product_code = 9801`(数字) | `rows=None`,`Extra=Using where` | **退化** ❌ | 强制规则(在工具内做类型规范化,**不交给模型自由传类型**): - `varchar` 列一律传**字符串**:编码类、`inventory_status`(实测为 `varchar(255)`,必须传 `'0'`/`'1'`); - `int` 列传 **int**:如 `warehouse_id`; - 禁止在 `where` 中对列做函数或类型转换。 ### 13.6 优化 5(零 DDL):组内嵌套改用 IN 收窄 实测仓库拆分查询出现 `Using temporary`。改为"先取本页产品码 → `where product_code in (本页产品码)` 查仓库拆分",把临时表规模限制在一页之内(实测单产品 21.8ms)。 ### 13.7 优化 6:查询超时 + 并发限流(防止拖垮库) - 实测配置 Druid `maxActive=20`(`ruoyi-admin/src/main/resources/application-dev.yml`),Agent 循环/并行调用会占满连接池。 - 措施:① 聚合 SQL 设置执行超时(MySQL 8 支持 `/*+ MAX_EXECUTION_TIME(3000) */` 或 JDBC `setQueryTimeout`),超时返回明确错误;② MCP 层对同一 bot 限流(如 60 次/分);③ **工具内不做并行查询**(沿用现有串行写法);④ 保留 `max_pages` 兜底。 ### 13.8 优化 7(可选):统计类查询路由从库 `application-dev.yml` 已有 `slave` 数据源占位(`enabled=false`)。分析类聚合可考虑路由只读从库,避免影响主库。 **限制**:主从延迟 → **财务金额不可走从库**;库存/汇总类可。需 DBA 确认从库可用性与延迟。 ### 13.9 优化 8:向 Agent 声明"支持的统计维度清单" 在 `metadata` 中声明允许的 `group_by` / `metrics` 枚举,清单外维度**明确不支持**,避免 Agent 用明细工具硬凑而触发全表扫描。 ### 13.10 仍然存在的边界(诚实声明) 1. SUMMARY 本身仍是 **1 次 O(N) 索引扫描**;不做预聚合表则无法做到亚秒级(当前 6.5 万行 ~38ms,百万级预计 ~0.6s,可接受); 2. **不支持跨表任意维度**(如"客户 × 产品 × 月份");那需要通用 SQL 能力,出于安全与性能不开放; 3. 若要亚秒级 + 固定维度统计,需**预聚合表 + 可靠定时任务**,属新增功能须单独评估;`oms_finance_operate_report` 的教训是:**必须有可靠的定时刷新机制,不能依赖手工触发接口**; 4. `include_total=true` 在大表上仍是额外 count 开销,故默认关闭。 ### 13.11 优化后的调用形态对照 | Agent 的问题 | 优化前 | 优化后 | |---|---|---| | "某产品还有多少库存" | 翻页累加,约 5 次调用 | **1 次** `mode=SUMMARY, group_by=NONE` | | "库存最多的 10 个产品" | 翻完 94 组后在模型侧排序 | **1 次** `group_by=PRODUCT, top_n=10` | | "本月采购到货率" | 翻页累加或模型计算 | **1 次** `group_by=ORDER, mode=SUMMARY`(含 `arrivalRate`) | | "这些订单还欠多少钱" | 多页累加 | **1 次** `group_by=ORDER, mode=SUMMARY` 或 `order_code_list` 精确查 | | "导出全部 SN 明细" | 648 页(被截断) | `mode=LIST` 且必须给范围;否则明确拒绝并提示收窄条件 | --- ## 十四、覆盖度缺口分析与完善(v6 分析 / v7 已补全) > 本章回答三个问题:统计维度覆盖了多少?三大域是否完整?当前是否算"最优"? > > **v7 状态:本章识别出的缺口已全部并入最终方案**——A 档(时间/状态/伙伴/负责人维度 + 时效度量)、B 档(`warehouse_list`/`purchase_list`/`finance_list` 三个列表工具,并把 v6 的 `purchase_order_detail`/`finance_bill_detail` 合并进去)、C 档(`inventory_flow.stock` 备货分组、`finance_order_position.charge` 计收分组)、D 档(P2 条件索引)均已落地。下面的矩阵保留作为**缺口审计记录与后续回归依据**。 ### 14.1 三大域覆盖度矩阵 **仓储域(7 个对象 / v11 已全部覆盖)** | 对象 / 表 | v6 判定 | 现状(v11) | |---|---|---| | 入库单 `oms_inventory_inner` | ✅ | ✅ `warehouse_list(INNER)` + `inventory_flow` | | 出库单 `oms_inventory_outer(+_detail)` | ✅ | ✅ `warehouse_list(OUTER)` + `inventory_flow` | | 发货单 `oms_inventory_delivery(+_detail)` | ✅ | ✅ `warehouse_list(DELIVERY)`;另有 manage 域 `ORDER_DELIVERY`(含签收) | | SN 条码明细 `oms_inventory_info` | ✅ | ✅ `inventory_sn_trace` / `inventory_stock_aggregate` | | 备货状态 `oms_stock_info` | ❌ | ✅ **v7 闭合**:`warehouse_list(STOCK)` + `inventory_flow.stock` | | 仓库主数据 `oms_warehouse_info` | ❌ | ✅ **v10 闭合**:`master_data_list(WAREHOUSE)` | | 单据范围查询(按状态/时间) | ❌ | ✅ **v7 闭合**:`warehouse_list` 支持状态/时间范围过滤 | **采购域(6 个对象 / v11 覆盖 5)** | 对象 / 表 | v6 判定 | 现状(v11) | |---|---|---| | 采购单 `oms_purchase_order(+_item)` | ✅ | ✅ `purchase_list(ORDER/ITEM)` + `purchase_arrival_aggregate` | | 采购单范围查询 | ❌ | ✅ **v7 闭合**:`purchase_list` 支持状态/时间/供应商 | | 供应商主数据 `oms_vendor_info` | ❌ | ✅ **v8 闭合**:`master_data_list(VENDOR)` | | 采购-订单绑定 `oms_purchase_order_map` | ❌ | ✅ **v8 闭合**:`purchase_list(ORDER_BIND)` | | 采购历史版本 | ❌ | ✅ **v8 闭合**:`purchase_list(HISTORY)` | | **采购审批待办 / 已办** | ❌ | **❌ 仍缺 → 见 16.9(实测可低成本闭合)** | **财务域(8 个对象 / v11 覆盖 6)** | 对象 / 表 | v6 判定 | 现状(v11) | |---|---|---| | 应收 / 应付 / 收款 / 付款 / 开票 / 收票 | ✅ | ✅ 单号点查 + 订单全景 + 5 流余额汇总 | | 计划表 / 明细表 / 核销表 | ✅ | ✅ `finance_order_position` / `finance_list` | | 计收 `oms_finance_charge` | ❌ | ✅ **v7 闭合**:`finance_list(CHARGE)` + `finance_order_position.charge` | | 财务单据范围查询 | ❌ | ✅ **v7 闭合**:`finance_list` 支持状态/时间/合作伙伴 | | 财务运营报表 `oms_finance_operate_report` | ❌(刻意) | **❌ 仍缺(刻意剔除物化表)→ 见 16.9:可用"按明细重算"补历史时点余额** | | 财务附件 `oms_fin_attachment` | ❌ | **⚠️ 可闭合为"附件元数据"→ 见 16.9** | ### 14.2 统计维度覆盖矩阵 | 维度 | v6 判定 | 现状(v11) | |---|---|---| | 产品 `PRODUCT` / 仓库 `WAREHOUSE` / 采购单 `ORDER` / 订单 `ORDER` / 无维度 `NONE` | ✅ | ✅ | | 时间维度(月/季) | ❌ | ✅ **v7 闭合**:`TIME_MONTH` / `TIME_QUARTER`(区间下推) | | 合作伙伴/客户 `PARTNER` | ❌ | ✅ **v7 闭合**(LIST 需 P2-11) | | 状态分布 `STATUS` | ❌ | ✅ **v7 闭合**(LIST 需 P2-14) | | 及时率 / 超期 | ❌ | ✅ **v7 闭合**:`ARRIVAL_DELAY_DAYS` / `OVERDUE_DAYS` | | 负责人/销售 `OWNER` | ❌ | **⛔ 主动移除**(无索引 + 业务价值未确认)→ 见 16.9 | | **账龄分桶(0-30/31-60/61-90/90+)** | — | **❌ 仍缺 → 见 16.9(零成本可补:`group_by=OVERDUE_BUCKET`)** | **结论(更新)**:统计维度覆盖 **v11 约 90%**;仅剩 **账龄分桶**(可零成本补)与 **OWNER**(主动移除)两项。 ### 14.3 最严重的缺口不是"统计维度",而是"列表能力"整体缺失 当前 8 个工具中,**所有单据类查询都强制"按单号点查"**(`outer_code` / `purchase_no_list` / `bill_code_list`)。直接后果是这些**最基础的问题无法回答**: - "本月有哪些采购单?" / "哪些采购单还没入库?" - "这个月发货了多少单?" / "哪些出库单还没确认?" - "本月开了多少票、收了多少款?" 这类问题在业务里出现频率极高,而当前方案要么拒绝、要么逼 Agent 用 SN/明细工具硬凑(必然触发大表扫描)。**这是比统计维度更优先要补的缺口。** ### 14.4 实测修正:我此前"无索引=不支持"的规则过严 实测行数分布推翻了"一刀切": | 规模档 | 表 | 全表扫描代价 | 结论 | |---|---|---|---| | 大表(线性增长) | `oms_inventory_info` 64,823;`oms_inventory_delivery_detail` 49,015 | 数十~数百 ms | **必须**严守索引约束 | | 小表(<1000 行) | 采购 941/951、出库 734/750、入库 580、发货 756、应收 627、应付 583、备货 526、计收 287、历史 121/142、供应商 17、仓库 14 | **<10 ms** | **可以**直接支持按状态/时间/伙伴过滤,无需索引 | **方案修正**:把"无索引 = 不支持该入口"改为**按表规模分级**: - 大表:只用索引列做入口(维持原约束); - 小表:允许状态/时间/伙伴维度的范围查询与统计(接受全表扫描),并标注"该表当前规模小,若增长需补索引"。 这一条修正同时解开了 14.1、14.2、14.3 的多数缺口。 > **技术注意**:时间维度聚合若写成 `date_format(create_time,'%Y-%m')`,**对列做函数会使索引失效**;正确做法是按区间下推(`create_time >= '2026-01-01' and create_time < '2026-02-01'`),未来若为大表加时间索引才有效。 ### 14.5 是否算"最优"?——分两个层面回答 | 层面 | 评价 | 依据 | |---|---|---| | **性能与实现质量** | **接近该架构下的上限** | 过滤键实测对齐索引、游标分页不重不漏、SUMMARY 一次算完、类型对齐防索引退化、超时限流;均有实测支撑 | | **业务覆盖完备性** | 原为**不完备**(统计维度约 60%、列表能力缺失);**v7 已补全至 ~95%** | 通过 A/B/C/D 四档补全:时间/状态/伙伴/负责人维度、三域列表查询、计收、备货、供应商主数据、采购-订单绑定、采购历史 | **并且"完备"与"轻量"本质冲突**:工具数越多,schema token 与模型选错率越高(当前 8 个已接近无路由时的上限)。因此不存在绝对最优,只有**按实际提问分布做取舍**。 ### 14.6 完善建议(按性价比分三档) **A 档 · 零新增工具(只扩参数枚举)→ 建议立即纳入** | 项 | 做法 | |---|---| | 时间维度 | 聚合类 `group_by` 增加 `TIME_MONTH` / `TIME_QUARTER`(区间下推实现) | | 状态分布 | 增加 `group_by=STATUS` | | ~~负责人维度~~ | **已移除**:`OWNER` 无索引支撑且业务价值未确认,见 15.2 | | 及时率/超期 | 度量增加 `OVERDUE_DAYS`(计划收款日 vs 今天)、`ARRIVAL_DELAY`(交货日 vs 实际入库日) | **B 档 · 补"域内列表查询"(补基础能力,建议每域 1 个,工具数 8 → 11)** | 工具 | 覆盖 | 过滤维度(分页) | |---|---|---| | `warehouse_list` | `entity = DELIVERY / OUTER / INNER / STOCK` | 单号、状态、时间范围、仓库、产品、合同号 | | `purchase_list` | `entity = PURCHASE_ORDER / VENDOR / ORDER_BIND / HISTORY` | 单号、状态、审批/确认状态、时间范围、供应商 | | `finance_list` | `entity = RECEIVABLE / PAYABLE / RECEIPT / PAYMENT / INVOICE / TICKET / CHARGE` | 单号、状态、审批状态、时间范围、合作伙伴 | > 用一个工具 + `entity` 参数而非每表一个工具,是为了在补全能力的同时把工具数增长压到最小。代价是单工具 schema 稍复杂。 **C 档 · 零新增工具,把高价值点查塞进现有工具** | 项 | 做法 | 支撑 | |---|---|---| | 计收 | `finance_order_position` 增加 `charge` 分组(计收状态、收入/成本/毛利) | `oms_finance_charge.order_code` 有**唯一索引** ✅ | | 备货 | `inventory_flow` 增加 `stock` 分组(备货状态、一次备齐) | 526 行,按 `order_code` 扫可接受 | | 采购-订单绑定 | `purchase_list` 的 `ORDER_BIND`(若要开,需 P2-1/P2-2 索引) | `oms_purchase_order_map` 仅主键 | **D 档 · 索引补充(仅当对应表增长时)** | 表 | 建议索引 | 触发条件 | |---|---|---| | `oms_stock_info` | `idx_order_code(order_code)` | 行数 > 10 万 | | `oms_finance_charge` | `idx_charge_status(charge_status)` | 行数 > 10 万 | | `oms_purchase_order` | `idx_status_date(status, purchase_date)` | 行数 > 10 万 | | `oms_inventory_outer/inner/delivery` | `idx_create_time(create_time)` | 行数 > 10 万(当前 580~756 行,**不必加**) | ### 14.7 补全后的代价与取舍(v7 已决策) | 选择 | 工具数 | 覆盖 | token / 选错率 | |---|---|---|---| | 原 v5 | 8 | 统计维度 60%、无列表能力 | 低 | | v6 拟定的 A+B+C | 11 | 三大域基本完整 | 中 | | **v7 采用** | **9** | 三大域覆盖完整(除审批待办、附件、运营报表);统计维度 ~95% | **低**(把 A/B/C 三档合并进 9 个工具,且用 `entity`/`group_by` 参数化而非新增工具) | | **v8 最终** | **10** | 三域 + **签收** + **撤回历史** + **主数据(批量编码翻译)** | **低**(仅新增 1 个主数据工具,机制复用) | **v7 决策**:采纳 **A + B + C + D** 全部四档,但通过 **①合并 v6 的 `purchase_order_detail`/`finance_bill_detail` 进 `purchase_list`/`finance_list`**、**②用参数枚举表达维度与单据类型** 两个手段,把工具数从 11 压回 **9**,在"覆盖完整"与"轻量"之间取得平衡。 ### 14.8 明确不覆盖的范围(v9 更新) 1. 采购/财务**审批待办与已办**(`bu_todo` 相关,属流程域); 2. **财务附件与文件内容**; 3. **财务运营报表物化表**(已否决,见 2.1); 4. ~~跨域任意维度~~ → **已改为"受限透视"**:仅白名单维度与单链路度量,见 16.2;**真正的任意 SQL / 无白名单组合仍不开放**; 5. ~~项目进度/POC/报价~~ → **已纳入** `project_list`,见 16.1; 6. 任何**写操作**(新增/修改/删除/审批/撤回/红冲)。 --- ## 十五、规格待定项定义(v8 补全) > 本章专门消除"实现时必然产生歧义"的规格空白。**上一版(v7)有 9 处未定义,本章逐条定义。** ### 15.1 时间维度依据字段(原文只说"支持 TIME_MONTH",未说基于哪个字段) | 工具 | 默认时间字段 | 可切换 | 索引依赖 | |---|---|---|---| | `inventory_stock_aggregate` | `oms_inventory_info.create_time` | — | P2-12(否则 1 次全表扫描) | | `purchase_arrival_aggregate` | `oms_purchase_order.purchase_date`(业务口径) | `time_field=CREATE_TIME` | P2-10 | | `finance_balance_aggregate` | 各单 `create_time`(记账口径) | `time_field=PLAN_DATE` → 应收取 `plan_receipt_date`、应付取 `plan_payment_date` | P2-11 系列 | - 默认区间:**近 12 个月**;跨度上限 **36 个月**;跨月/季一律用**区间下推**(`>= 月初 and < 下月初`),**禁止 `date_format(create_time,...)`**(会使索引失效)。 ### 15.2 维度可用性矩阵(v8 修正:解决"声称支持但无索引支撑"的矛盾) 区分两种模式:**SUMMARY 只需 1 次扫描,维度不受索引限制;LIST 的分页游标必须与索引顺序一致。** | `group_by` | SUMMARY | LIST(游标分页) | 依赖索引 | |---|---|---|---| | `NONE` | ✅ | —(不分页) | — | | `PRODUCT` | ✅ | ✅ | `oms_inventory_info.idx_product_code` | | `WAREHOUSE` | ✅ | ✅ | 库存大表需 **P2-7** | | `PRODUCT_WAREHOUSE` | ✅ | ⚠️ 需 **P2-13** `(warehouse_id, product_code)`,否则 filesort | P2-13 | | `STATUS` | ✅(1 次全表扫描,实测 ~40ms @6.5 万行) | ⚠️ 需 **P2-14** `(inventory_status, product_code)` | P2-14 | | `TIME_MONTH` / `TIME_QUARTER` | ✅ | ⚠️ 需 P2-12 / P2-10 | P2-12 / P2-10 | | `ORDER` | ✅ | ✅ | 各表 `order_code` 索引 | | `VENDOR` | ✅ | ✅ | **P1-2** | | `PARTNER` | ✅ | ⚠️ 需 **P2-11** | P2-11 | | **`OVERDUE_BUCKET`(v13 新增)** | ✅ | ⚠️ **仅 SUMMARY**(分桶后行数固定 ≤4,无需 LIST 分页) | — | **统一规则**: 1. **LIST 模式下,若该维度无索引支撑 → 不静默 filesort,而是返回 `INVALID_PARAMS` 并提示"该维度请使用 `mode=SUMMARY`"**(避免深分页把库拖垮); 2. **移除 `OWNER` 维度**:`oms_purchase_order.owner_name` 无索引且业务价值未确认(v7 的 A 档曾声称支持,属方案自相矛盾,此处更正)。 ### 15.3 `arrival_delay_days` 语义与成本(原文未定义) - **定义**:对每条采购明细行,`delay = datediff(该明细首次入库时间, item.delivery_date)`;采购单维度取 **`max(delay)`**(最晚到货);未入库的明细不计入;无入库记录返回 `null`。 - **首次入库时间来源**:`oms_inventory_inner`(按 `purchase_no` 取 `min(create_time)`)按 `product_code` 与 `oms_inventory_inner_detail` 对齐。 - **成本与前提**:`oms_inventory_inner.purchase_no` **无索引** → 需 **P2-15**;**默认不计算**(仅当 `metrics` 显式包含 `ARRIVAL_DELAY_DAYS` 时才发起该 join),并在 `metadata` 标注该度量的额外成本。 ### 15.4 `sub_cursor` 协议(原文只提名字,未定义用法)—— ✅ 已实现 | 项 | 约定 | |---|---| | 入参回传 | `sub_list`(子列表名)+ `sub_cursor`(字符串);`sub_cursor` 与主 `cursor` **互斥**;首次翻页无游标时须给 `sub_parent`(父实体标识),`sub_parent` 也随游标携带,续页时无需再传 | | 返回 | `data` = `{sub_list, sub_parent, total(该子列表总条数), items(本页), sub_page_info}`;`sub_page_info` 结构与 `page_info` 完全一致,`sort_by` 为该子表的排序键 | | 编码 | 同主游标格式,但 `t = ":"`;`f` 中额外包含主实体标识(`inventory_flow` 为 `outer_code`,`finance_order_position` 为账单号);`k[0]` = 续页偏移量 | | 返回游标位置 | 主响应截断时 `data.truncated_sub_lists[]` 给出 `{list, parent, total, returned, next_cursor}`,调用方**直接回传** `next_cursor` 即可续页(不需要自己算偏移) | | 上限 | 子列表 `page_size ≤ 100`;`max_pages ≤ **200**`(**原文为 20,实测不足**:20×100=2000 行 < 实测单出库单 2682 条 SN 明细,会导致"截断 + 游标也取不完"的数据缺失;改为 200 后单父实体最多可取 20,000 行) | | 幂等性 | 主实体不变时同一 `sub_cursor` 可重复调用且结果稳定(实现上按子表主键 `id` 升序定序后切片;部分子表 SQL 无 `order by`,故**在内存中按 id 统一定序**,保证"截断点 == 续页起点") | | 覆盖范围 | `inventory_flow`:`outerDetails` / `snDetails` / `deliveries`;`finance_order_position`:`receiptPlans` / `receiptDetails` / `invoicePlans` / `paymentPlans` / `paymentDetails` / `ticketPlans` | | 明确不覆盖 | 嵌套二级子列表(如 `deliveries[].productSns`)不单独提供游标;如需按物流单追溯 SN,请用 `warehouse_list(entity=SN)` 或 `inventory_flow` 的 `snDetails` 子列表 | ### 15.5 `metrics` 全枚举 与 参数冲突规则(原文只给示例) **metrics 全枚举** | 域 | 取值 | |---|---| | 库存 | `QTY`、`IN_STOCK_QTY`、`OUT_STOCK_QTY`、`INNER_AMOUNT`、`OUTER_AMOUNT` | | 采购 | `PURCHASE_QTY`、`INNER_QTY`、`PENDING_QTY`、`ARRIVAL_RATE`、`ARRIVAL_DELAY_DAYS`、`AMOUNT_TOTAL`、`TAX_TOTAL` | | 财务 | `RECEIVABLE_*`、`RECEIVED_*`、`UNRECEIVED_*`、`INVOICED_*`、`UNINVOICED_*`、`PAYABLE_*`、`PAID_*`、`UNPAID_*`、`TICKETED_*`、`UNTICKETED_*`、`OVERDUE_DAYS`(`*` ∈ `WITH_TAX` / `WITHOUT_TAX` / `TAX`) | **参数冲突规则(一律返回 `INVALID_PARAMS` 并回显允许值,不静默忽略)** | 冲突组合 | 处理 | |---|---| | `mode=SUMMARY` + `cursor` / `page_size` | 报错(SUMMARY 不分页) | | `mode=LIST` + `top_n` | 报错(`top_n` 仅 SUMMARY 可用) | | `cursor` + `page` | 报错 | | `mode=LIST` 且聚合工具未给范围 | 报错并提示改用 `SUMMARY` | | `metrics` 含未定义值 | 报错并回显允许枚举 | | `include_detail=false` + `code_list`(列表工具) | 允许(等价于按单号查表头) | | `group_by=OVERDUE_BUCKET` + `mode=LIST` | 报错(分桶行数固定 ≤4,只支持 SUMMARY) | | `as_of_date` 早于最早单据日期 | 允许,返回 0 并在 `metadata` 标注 | ### 15.6 `include_zero` 判定标准(原文未定义) - **定义**:分组内**全部**所请求 `metrics` 的值均为 `0` 或 `null` → 视为"全零行"; - 默认 `include_zero=false` → 过滤掉全零行;置 `true` 则返回; - 判定基于**本次请求的 `metrics` 集合**,而非固定字段集,避免语义歧义。 ### 15.7 "在库"口径(⚠️ **待业务确认**) | 候选 | 定义 | 风险 | |---|---|---| | **A(当前默认)** | `inventory_status='0'` 的全部 SN 视为在库 | 若业务含"已被发货单占用未出库"的占用量,则会**高估在库** | | B | 在 A 基础上排除已被发货单占用但未出库的 SN(需 join `oms_inventory_delivery(_detail)` / `delivery_list`) | 成本更高,需确认占用判定规则 | - **当前处理**:默认按 A,并在 `metadata.aggregation_rule` 明确写出"在库 = `inventory_status='0'`,**未扣除已发货占用**"; - 待业务确认后若改为 B,需同步补索引并重新评估性能。 ### 15.8 含税 / 未税口径(⚠️ 待确认清单) | 字段 | 实测现状 | 处理 | |---|---|---| | `oms_inventory_info.inner_price` / `outer_price` | 列注释仅"入库价 / 出库价",**无"含税"字样** | 标注**待确认**,`metadata` 中**不得**写"含税"(v7 曾自行断言为含税,此处更正) | | `oms_inventory_inner_detail.inner_price` | 列注释明确"入库单价(**含税**)" | 可作为入库明细的含税口径依据 | | 采购 / 财务金额 | `total_price_with_tax` / `..._without_tax` 字段名自解释 | 直接采用 | **原则**:字段名未自解释的,一律标注待确认,**禁止在 `metadata` 中自行断言口径**。 ### 15.9 多币种策略 - 采购 / 财务均有 `currency` 字段(实测固定人民币); - **不自动换算**,按原币返回;一次聚合跨多种币种时返回 `metadata.mixed_currency=true` 并提示按币种分组查看; - 不引入汇率表(属新增功能,超出范围)。 ### 15.10 大小写与排序规则(实测发现,原方案未考虑) - 实测:`oms_inventory_info` / `oms_inventory_outer` / `oms_receivable_bill` / `oms_inventory_delivery` 等表的 collation 均为 **`utf8mb4_unicode_ci`** → **大小写与重音不敏感**,即 `where outer_code = 'c-xxx'` 会匹配到 `C-XXX`; - **策略**:编码类过滤保持现状(与页面行为一致),但在 `metadata` 声明"编码匹配不区分大小写";如需严格区分,仅对单号精确点查提供 `exact_case=true` → SQL 使用 `where binary outer_code = :code`。 ### 15.11 运行参数与可观测性(原文缺失)—— 超时/限流 ✅ 已实现 | 项 | 约定 | |---|---| | 查询超时 | ✅ 聚合类 **3000ms**、列表/点查类 **5000ms**。实现:`McpService` 在调用工具前按工具名(`*_aggregate` → 3s,其余 5s)写入 `McpQueryTimeout` 线程上下文,`McpQueryTimeoutInterceptor`(MyBatis `StatementHandler.prepare` 插件)读取后 `Statement.setQueryTimeout(n)`;未设置上下文的普通页面/报表 SQL **不受影响**。超时由驱动 `KILL QUERY` 中止并映射为 **`-32003 query_timeout`** | | 限流 | ✅ 同一机器人 **60 次/分**(滑动窗口)。**实现偏离原文**:项目未引入 Redis,故落为**进程内** `McpRateLimiter`(key = `X-Bot-Id`,无凭证时退回绑定用户/匿名;命中返回 **`-32002 rate_limit_error`**)。多实例部署时为单实例口径,若需全局精确限流须改 Redis | | 日志埋点 | ⏳ 未实现(`tool`/`entity`/`mode`/`duration_ms`/`rows`/`page_no`/`has_more`/`truncated_by_bytes`/`filter_hash`) | | schema 版本 | `metadata.schema_version = 1`(以代码常量为准;破坏性变更时递增) | | 类型规范化 | `varchar` 一律 `String.valueOf()` 传入;`int` 一律 `Integer/Long`(见 13.5 实测) | ### 15.12 备份表与白名单(防误用,实测发现风险) 实测 `oms_test` 存在大量备份/历史表,**必须显式排除**,任何工具不得指向: `oms_inventory_info_copy1`(**42,836 行**)、`delivery_list_0618`(11,659)、`product_info_260916`、`product_info_20260904bak`、`project_info_20260904bak`、`project_info_0707`、`project_order_info_1028` / `_0627` / `_bak1`、`project_product_info_bak` / `_0708` / `_0627`、`order_info_0707` / `_0708` / `_bak`、`oms_purchase_order_1211`、`oms_payable_bill_copy1`、`oms_finance_operate_report*` 等。 **另有两张"看起来能用但已确认不采用"的表(v10 补充)**: | 表 | 行数 | 处理 | |---|---|---| | `vendor_info` | 5 | **明确不采用**:供应商主数据**只用 `oms_vendor_info`**(17 行,字段含账期/银行/省市);不因查不到编码而回退此表 | | `oms_inventory_inner_detail` | 1 | **不采用**:定义上是入库产品行,但实测仅 1 行(未启用);**入库明细以 `oms_inventory_info`(按 `inner_code`)为准** | **实现要求**:工具内以**表白名单**方式声明可访问表(而非黑名单),新增 entity 必须显式登记;`oms_inventory_info_copy1` 行数与正式表同量级,误用后果严重。 ### 15.13 测试数据与压测方案(原文缺失) - **边界数据**:部分入库的采购单、多仓发货的出库单、已签收/未签收的 manage 域发货单、超期未收的应收单、已撤回的发货单、`tax_rate` 为 NULL 的 SN、软删除(`deleted_at` 非空)的发货明细; - **压测用例**:`inventory_sn_trace`(50 SN)、`warehouse_list(entity=SN)`(连续翻页 1 万行)、三个聚合工具的 `SUMMARY`(`group_by=NONE` 与 `top_n=10`),逐一记录 P95; - **并发验证**:模拟 3 个并发 bot 调用,确认 Druid `maxActive=20` 不被占满、限流与超时按预期触发。 ### 15.14 编码与枚举的实测异常(v11 新增) **① 编码字段存在脏数据(前导制表符)** - 实测:`order_info.order_code` 有 **14 行**以制表符 `\t` 开头(`project_order_info` 为 0 行); - 影响:直接 `=` 或 `join` 会漏关联;且 `utf8mb4_unicode_ci` 下部分控制字符被折叠,导致**同一条件下 `EXISTS` 与 `LEFT JOIN` 结果不一致**(实测 `EXISTS` 命中 330 行,而 `LEFT JOIN` 取前 5 行均为 null); - 处理:**所有编码比对与 join 一律 `trim()`**;返回结果中的编码也 `trim()` 后输出,避免 Agent 看到脏值。 **② 枚举实际值与列注释不符** - `order_info.order_type` 列注释为"1-直签合同,2-代理商合同",**实测实际值是 `zq`(205) 与 `dls`(161)**(推测 `zq`=直签、`dls`=代理商,**需业务确认**); - 处理:`orderTypeName` **按实际值 `zq`/`dls` 映射**,不得按注释的 1/2;并在 `metadata` 标注"取值与列注释不一致,已按实测值映射"; - `order_info.status` 实测 0(345)/1(21),与注释"0-有效,1-无效"一致 ✅;且 `status=0` 的行数**恰等于** `deleted_at is null` 的 345 行 → 两者语义重合,**工具按 `deleted_at is null` 过滤即可**。 **③ 两套订单模型只能部分对齐**:`order_info` 去空白后仅 **330/366** 能命中 `project_order_info` → 必须容忍关联不到(详见 16.1)。 **④ 通用规则(并入 15.5 的类型规范化)**:所有 `varchar` 编码的**入参与出参统一 `trim()`**;发现脏数据不得静默忽略,需在 `metadata.data_quality` 提示(如 `{ "trimmed_codes": 14 }`)。 **⑤ 实测补充(v14,实现期发现,务必遵守)** | 项 | 实测结论 | 处理 | |---|---|---| | **MySQL `TRIM()` 不去制表符** | `trim(order_code) in ('ZGXV-20250716SCS001')` 命中 **0** 行,而 `trim(replace(order_code,'\t',''))` 命中 1 行 | `order_info` 的编码比对/游标/排序**三处统一**用 `trim(replace(order_code,'\t',''))`;Java 侧出参用 `trim()` 即可 | | **`order_delivery.delivery_status` 取值为拼音缩写** | 实测 `qs`(324)/`yf`(31),**不是**列注释的 1/2/3 | 翻译按实测值:`qs`=已签收、`yf`=已发货,并保留数字 1/2/3 兜底 | | **`purchase_order_map.order_id` 指向 `project_order_info`** | 实测 1116/1123 命中 `project_order_info`,仅 28 条巧合命中 `order_info` | 该表的绑定关系 join `project_order_info.id`;**注意与 `order_delivery.order_id`(→`order_info`)是两条不同链路** | | **`oms_inventory_info` 无 `purchase_no` 列** | 列清单中不存在(Java 实体字段为派生值) | SN 的采购单号经 `inner_code` 关联 `oms_inventory_inner.purchase_no` 批量补齐,不在 SN 表直取 | | **`oms_receivable_bill` 无 `plan_receipt_date` 列** | 该列在 `oms_receivable_receipt_plan` | 账龄分桶经 `last_receipt_plan_id` 关联收款计划取 `plan_receipt_date`;无计划的行归入 `NO_PLAN` 桶 | | **`/mcp` 响应头 `Transfer-Encoding: chunked` 重复(既有问题)** | `McpController` 手工 `setHeader("Transfer-Encoding","chunked")` 后 Tomcat 再补一次 → 出现两个同名头,**curl 会丢弃响应体**(Python `http.client` 正常) | **v15 已修复**:移除手工设置那一行;修复后 `curl` 可直接调试 `/mcp`(实测 HTTP 200 + 正常响应体) | | **`java.time.LocalDateTime` 序列化缺失(v15 实测)** | SQL 以 `Map` 返回时 `DATETIME` 列可能是 `LocalDateTime`(如 `order_delivery.sign_time`),`McpController` 的裸 `ObjectMapper` 未注册 jsr310 → `InvalidDefinitionException`,且被外层 `catch` 吞掉 → **HTTP 200 + 空响应体**(极难排查) | **v15 已修复**:`McpController` 注册 java.time 序列化器(日期 `yyyy-MM-dd`、时间 `yyyy-MM-dd HH:mm:ss`,与工具约定一致);同时把外层 catch 改为**回写 error 响应**,不再静默吞异常 | | **空列表导致 `where col in ()`(v15 实测)** | `inventory_sn_trace` 对**未提供**的入口也发起查询 → `in (...)` 空列表是**非法 SQL**(`SQLSyntaxErrorException`) | **v15 已修复**:Java 侧仅对非空列表发起查询(同时减少无用查询) | | **SQL 引用不存在的列(v15 实测)** | 3 处:`oms_inventory_outer.receivable_bill_code`(列不存在)、`oms_payable_bill.vendor_name`(只有 `vendor_code`)、`project_order_info.project_code/project_name`(在 `project_info` 上) | **v15 已修复**:分别删除该列、改经 `oms_vendor_info` 关联取名称、改用 `project_info` 的列。并新增**系统性校验脚本**:解析新增 select 的全部 `别名.列` 与 `information_schema` 比对(77 条语句 → 最终 0 处不存在列) | --- ## 十六、扩展覆盖与工程完善(v9) > 本章补齐上一版的 3 类遗留:**① 覆盖缺口**(跨域任意维度、项目进度/POC/报价);**② 业务口径从"待确认"变为"可配置化落地"**;**③ 工程项**(索引 DDL 执行方案、时间区间策略、从库路由、RAG 工具路由)。 ### 16.1 新增覆盖 A:项目 / POC / 进度 / 报价 / manage 域合同 → 新工具 `project_list` **为什么单独成工具**:这属于"项目域",与仓储/采购/财务的单据结构差异大;塞进 `master_data_list` 会让 schema 更混乱。 **入参**:`entity`(必填)、`code_list`、`project_id_list`、`time_range`、`name_like`、`stage_list`、`include_detail`、`page_size`、`cursor` | entity | 主表 | 关键返回字段 | 索引 | |---|---|---|---| | `PROJECT` | `project_info`(2,212) | `projectCode`/`projectName`、`customerCode`/`customerName`、`agentCode`代表处、`partnerCode`/`partnerName`代理商、`industryType`行业、`bgProperty`BG、`projectStage`项目阶段、`projectGraspDegree`把握度、`estimatedAmount`预计金额、`currencyType`、`estimatedOrderTime`/`estimatedDeliverTime`、`competitor`竞争对手、`countryProduct`是否国产、`jointTrial`是否会审、`poc`、`hzSupportUser`、`operateInstitution`、`keyProblem`、`projectDesc` | `unq_idx(project_code)`、`idx_customer_code`、`idx_agent_code` ✅;按 `partner_code` 需 **P2-24** | | `PROJECT_PRODUCT` | `project_product_info`(2,809) | `productBomCode`、`model`、`productCode`、`productDesc`、`quantity`、`cataloguePrice`目录价、`catalogueAllPrice`目录总价、`price`/`allPrice`、`guidanceDiscount`、`discount`、`taxRate` | `idx_project_id` ✅;按产品编码跨项目查询需 **P2-23** | | `PROGRESS` | `project_work_progress`(7) | `projectId`、`workContent`变更内容、`workUser`更新人、`workTime`更新时间 | `idx_project_id` ✅ | | `POC` | `project_poc_info`(998) + `_detail`(2) | `projectId`、`serverConfig`、`terminalConfig`、`operateSystem`、`vdiVersion`、`processPerson`/`processPhone`研发、`handlePerson`/`handlePhone`现场、`hzInterfacePerson`/`hzInterfacePhone`、`startDate`、`h3cPerson`、`planFinishTime`/`realFinishTime`;明细 `testProgress`测试进展 | 仅 PRIMARY → 需 **P2-21** `(project_id)` | | `QUOTATION` | `oms_quotation`(0) + `oms_quotation_product_info`(0) | `quotationCode`报价单号、`quotationName`、`quotationAmount`/`discountAmount`、`quotationStatus`、`agentCode`、`amountType`币种、`customerName`;明细产品行同 `PROJECT_PRODUCT` 结构 | 仅 PRIMARY → 需 **P2-22**(两张表各 1 条) | | **`CONTRACT`(v11 新增)** | **`order_info`**(366) | **manage 域合同**:`orderCode`合同编号、`versionCode`版本号、`projectCode`关联项目编号、`orderName`合同名称、`customerCode`/`customerName`/`customerAddress`/`customerContact`/`customerPhone`/`customerEmail`、`orderType`/`orderTypeName`合同类型、`orderAgentCode`代表处编码、`bgType`BG属性、`orderPartnerCode`代理商编码、`orderDate`合同签订日期、`status`/`statusName`合同状态、`industryType`一级行业、`customerPostcode`、`remark`、`createdAt`/`updatedAt`;**默认过滤 `deleted_at is null`** | `uk_order_code(order_code, version_code)`(UK) ✅ 可按合同号查 | | **`CONTRACT_PRODUCT`(v11 新增)** | **`order_list`**(854) | `orderId`合同ID、`productCode`BOM编码、`quantity`数量、`price`单价、`discount`折扣、`amount`总价、`remark`、`createdAt`/`updatedAt`、`status`数据状态;**默认过滤 `deleted_at is null`** | `idx_order_id`、`idx_product_code` ✅ | > **两套订单/合同模型的关系(v11 实测,务必按此实现)** > > | 体系 | 主表 | 明细 | 发货 | 说明 | > |---|---|---|---|---| > | **manage 域(销售/交付侧)** | `order_info`(366) | `order_list`(854) | `order_delivery`(355) + `delivery_list`(32,718) | `order_delivery.order_id` **355/355 指向 `order_info`** | > | 项目域 | `project_order_info`(809) | `project_product_info`(2,809) | — | 由现有 `project_order_info` 工具与 `project_list` 覆盖 | > > 1. **禁止按 `id` 跨体系关联**:`order_delivery.order_id` 只能关联 `order_info.id`(按 id 关联 `project_order_info` 会有 242 条"假命中",属数值巧合); > ⚠️ **v14 补充**:`oms_purchase_order_map.order_id` 恰好相反——实测 **1116/1123 指向 `project_order_info.id`**(仅 28 条巧合命中 `order_info`),故 `purchase_list(entity=ORDER_BIND)` 按 `project_order_info` join。**两张表的 `order_id` 语义不同,切勿套用同一条链路。** > 2. **`order_code` 只能"部分对齐"**:去空白后 `order_info` 中 **330/366** 能命中 `project_order_info`,**36 行命中不了** → 工具必须**容忍关联不到**,`metadata` 中标注"该合同在项目域无对应记录",**不得报错也不得臆造**; > 3. 对齐时**必须 `trim()`**:`order_info` 有 **14 行** `order_code` 带前导制表符(脏数据),不 trim 会漏关联(见 15.14)。 > **价值**:补齐"按合同号点查订单/项目""这个项目的 POC 到哪一步了""报价单有哪些产品"等此前完全无法回答的问题(`project_order_info` 现有工具仅支持按创建时间范围查询)。 ### 16.2 新增覆盖 B:跨域任意维度(客户 × 产品 × 月)→ 新工具 `cross_domain_aggregate` **先说结论**:**真正的"任意维度"不可能安全实现**(等价于开放任意 SQL,性能与安全都不可控)。因此本方案提供的是**受限透视(restricted pivot)**:维度与度量来自**白名单**,且**度量必须属于同一条数据链路**。 **入参**:`dimensions`(1–3 个,白名单)、`metrics`(白名单)、`time_range`(**必填**,≤36 个月)、`time_granularity`(`MONTH`/`QUARTER`)、`top_n`(默认 20,上限 200)、`filters` **维度白名单**:`PARTNER`(客户/进货商)、`CUSTOMER`、`AGENT`(代表处)、`PRODUCT`、`PROJECT`、`WAREHOUSE`、`MONTH`、`QUARTER` **度量白名单与数据链路(关键:同链路才可混合)** | 链路 | 事实表 / 连接路径 | 可用度量 | |---|---|---| | `SALES` | `project_product_info` ⋈ `project_order_info`(→`project_id`、`order_code`、`partner_code`)⋈ `project_info`(→`customer_code`、`agent_code`) | `SALES_AMOUNT_WITH_TAX`、`SALES_AMOUNT_WITHOUT_TAX`、`SALES_QTY` | | `PURCHASE` | `oms_purchase_order_item` ⋈ `oms_purchase_order` | `PURCHASE_QTY`、`PURCHASE_AMOUNT`、`PURCHASE_TAX` | | `STOCK` | `oms_inventory_info` | `IN_STOCK_QTY`、`OUT_STOCK_QTY`、`INNER_AMOUNT` | | `FINANCE_AR` | `oms_receivable_bill` | `RECEIVABLE_WITH_TAX`、`RECEIVED_WITH_TAX`、`UNRECEIVED_WITH_TAX`、`INVOICED_WITH_TAX`、`UNINVOICED_WITH_TAX` | | `FINANCE_AP` | `oms_payable_bill` | `PAYABLE_WITH_TAX`、`PAID_WITH_TAX`、`UNPAID_WITH_TAX`、`TICKETED_WITH_TAX`、`UNTICKETED_WITH_TAX` | **典型问题映射**: - "客户 × 产品 × 月 的销售额" → `dimensions=[CUSTOMER, PRODUCT, MONTH]`、`metrics=[SALES_AMOUNT_WITH_TAX]`、链路 `SALES`; - "代表处 × 月 的未收款" → `dimensions=[AGENT, MONTH]`、`metrics=[UNRECEIVED_WITH_TAX]`、链路 `FINANCE_AR`。 **护栏(必须)**: 1. **`time_range` 必填**(否则拒绝),跨度 ≤ 36 个月 → 保证走时间区间下推而非全表; 2. 至少一个维度**有索引支撑**;否则要求 `allow_full_scan=true` 显式确认(默认拒绝); 3. 度量**跨链路混用直接报错**(如同时要 `SALES_AMOUNT_WITH_TAX` + `UNRECEIVED_WITH_TAX`);如需,须分两次调用; 4. 分组数上限 `max_groups=1000`,超出报错并提示收窄; 5. **`mode=SUMMARY`(默认)** 返回 Top-N + 全局合计;`mode=LIST` 走游标分页,且维度必须与索引顺序一致(沿用 15.2 规则); 6. 结果必须带 `metadata.lineage`(说明本结果来自哪条链路与哪些表),避免 Agent 误读口径。 **成本提示**:`SALES` 链路是 **3 表 join + 分组**,是本方案最重的查询;实测规模下预计 **数百 ms 级**,在百万行级需要 `project_order_info(order_code)`(已有)与 `project_product_info(project_id)`(已有)支撑;时间维度需 **P2-25**。文档中必须标注"该工具为**兜底能力**,优先使用域内专用工具"。 ### 16.3 业务口径:从"待确认"改为"可配置化落地"(解决 15.7 / 15.8 阻塞) > ⚠️ **2026-09-23 状态更新:本节设计已作废(不予采用)**。曾按本节实现 `mcp.inventory.stock-basis` / `price-basis` 配置项(application.yml + `@ConfigurationProperties` + 3 个聚合查询的 `` 分支 + 工具参数覆盖),经确认**不引入配置机制,已全部回滚**。当前实际状态:口径**写死在 SQL 与 metadata 声明中**,业务确认后需改代码(见第十二章遗留 9/10)。本节以下内容仅作为历史设计记录保留。 原则:**口径不确定时不阻塞编码,而是做成配置项 + 参数 + 校验 SQL**,业务确认后改配置即可,**不改代码**。 | 口径 | 配置项(application.yml) | 工具参数(可选覆盖) | 元数据行为 | 业务校验 SQL(可直接执行) | |---|---|---|---|---| | 在库(15.7) | `mcp.inventory.stock-basis: IN_STOCK_ONLY`(默认) / `EXCLUDE_DELIVERED` | `stock_basis` | `metadata.aggregation_rule` 写明当前口径与"是否扣除已发货占用" | ① `select count(*) from oms_inventory_info where inventory_status='0'`;② 扣除占用:`... and product_sn not in (select d.product_sn from oms_inventory_delivery_detail d join oms_inventory_delivery m on m.id=d.delivery_id where m.delivery_status in ('0','1'))` → 两者差值即"占用量",据此判断口径 | | 价格含税(15.8) | `mcp.inventory.price-basis: UNKNOWN`(默认) / `WITH_TAX` / `WITHOUT_TAX` | — | 仅当配置为 `WITH_TAX`/`WITHOUT_TAX` 时,元数据才声明含税/未税;`UNKNOWN` 时字段注释只写"入库价/出库价" | 取同一 SN 与采购单明细比对:`select i.product_sn, i.inner_price, pi.amount_total/pi.quantity as po_unit_price, pi.tax_rate from oms_inventory_info i join oms_purchase_order_item pi on pi.product_code=i.product_code limit 20` → 若 `inner_price ≈ po_unit_price` 则为含税,若 `inner_price ≈ po_unit_price/(1+tax_rate)` 则为未税 | > 这两条把"业务确认"从**阻塞项**降级为**配置项**,可立即进入编码;上线前由业务跑一次校验 SQL 决定配置值。 ### 16.4 索引 DDL 执行方案(执行项 1) **执行顺序与窗口** | 阶段 | 内容 | 窗口 | 影响 | |---|---|---|---| | 预检 | 在生产/正式测试库执行 `SHOW INDEX FROM
` 复核 2.5 节结论;确认无同名索引 | 任意 | 只读 | | 第 1 步 | P0 两条(`oms_inventory_outer.outer_code`、`oms_inventory_outer_detail.outer_code`) | 低峰 | 表小(734/750 行),秒级 | | 第 2 步 | P1-1、P1-2(`oms_inventory_inner.order_code`、`oms_purchase_order.vendor_id`) | 低峰 | 表小,秒级 | | 第 3 步 | **P1-3 覆盖索引**(`oms_inventory_info` 6.5 万行) | 低峰,避开入库高峰 | 在线 DDL,`ALGORITHM=INPLACE, LOCK=NONE`;**需评估写入放大** | | 第 4 步 | P2 系列 | 按触发条件(表 > 10 万行)再执行 | — | **统一 DDL 形态(含在线参数)** ```sql ALTER TABLE
ADD INDEX (), ALGORITHM=INPLACE, LOCK=NONE; ``` **体积与耗时估算方法**(执行前评估,不靠猜) - 索引体积 ≈ `行数 × (键长 + 主键长 + 约 15 字节开销)`。P1-3 键长约 `product_code(≤255, 实测样例 8~12 字符) + inventory_status(1) + 2×decimal(10,2)` ≈ 60–80 字节 → 6.5 万行约 **5–8 MB**;百万行约 **80–120 MB**(需与 DBA 确认磁盘)。 - 在线加索引耗时 ≈ 全表扫描 + 排序一次的量级;`oms_inventory_info` 单次全表聚合实测 ~40ms,故预计 **秒级~分钟级**。 **验证与回滚** - 验证:`SHOW INDEX FROM
WHERE Key_name=''` + 对目标语句 `EXPLAIN` 确认 `key` 命中且 `type` 非 `ALL`; - 回滚:`ALTER TABLE
DROP INDEX , ALGORITHM=INPLACE, LOCK=NONE;`(随时可执行)。 ### 16.5 时间维度区间的可配置策略(执行项 2) | 配置项 | 默认 | 说明 | |---|---|---| | `mcp.aggregate.time-range.default-months` | `12` | 未传 `time_range` 时的默认窗口 | | `mcp.aggregate.time-range.max-months` | `36` | 请求上限,超出报 `INVALID_PARAMS` | | `mcp.aggregate.time-range.large-window-threshold` | `24` | 超过该值且涉及**大表**时,要求显式 `allow_large_scan=true` | **结论**:**不放开默认区间**。若业务确需 3 年以上历史,两条路径①先加 P2-12/P2-10 时间索引(推荐)②在加了索引前以 `allow_large_scan=true` 承担扫描成本。 ### 16.6 从库路由实现方案(执行项 3) **项目已具备能力(实测确认)**:`DynamicDataSource`、`DataSourceType`、`@DataSource`、`DataSourceAspect`、`DynamicDataSourceContextHolder`;`application-dev.yml` 中 `slave.enabled=false`(默认关闭)。 **实现方式(推荐:工具内显式切换,而非依赖 AOP)** - 原因:`@DataSource` 是 AOP 注解,只在 **Spring Bean 方法调用**上生效;MCP 工具直接调 Service/Mapper,用注解易失效; - 做法:在公共基类中按配置决定是否切换: ``` if (readonlyRouteEnabled(toolName)) { DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE); try { ...执行查询... } finally { DynamicDataSourceContextHolder.clearDataSourceType(); } // 必须清理,线程池复用会串库 } ``` **路由白/黑名单(关键约束)** | 允许走从库 | 禁止走从库 | |---|---| | `inventory_sn_trace`、`inventory_flow`、`inventory_stock_aggregate`、`warehouse_list`、`purchase_*`、`project_list`、`master_data_list` | **所有财务类工具**(`finance_*`、`finance_order_position`、`finance_balance_aggregate`、`cross_domain_aggregate` 的 `FINANCE_*` 链路)—— 金额不允许受主从延迟影响 | - 配置:`mcp.datasource.readonly-route.tools: <逗号分隔白名单>`,`enabled: false` 默认全走主库; - 前置条件:需 DBA 提供可用从库并确认**延迟指标**(建议延迟 > 1s 时自动降级回主库,通过定时探活实现); - 收益/代价:把"统计类聚合"从主库剥离;代价是**读到的数据可能滞后**,需在 `metadata` 标注 `data_source: SLAVE`。 ### 16.7 RAG 工具路由(补上 `prompt.md` 未实现部分) **现状问题**:`tools/list` 全量下发 12 个工具(10 新 + 2 现),每轮 schema token 高、模型选错率上升。 **实现方案(新增 3 个类,放在 `com.ruoyi.sip.llm`)** | 类 | 职责 | |---|---| | `ToolEmbedding` | `toolName` / `description` / `embedding(double[])` | | `ToolRetriever` | `@PostConstruct` 时从 `McpToolRegistry.list()` 建内存索引;提供 `retrieve(query, topK)` | | `ToolRouter` | 接收用户问题 → 调 `retriever` → 返回候选工具集(**不返回全量**,默认 topK = 5) | **中文场景下的向量化(关键,不能用英文分词照搬)** - 分词:中文用**字符 2-gram**(`"库存汇总"` → 库存/存汇/汇总)+ 英文/编码按空格与驼峰切分; - 加权:**关键词命中加权**(同义词表)+ TF-IDF 余弦相似度;不依赖外部 embedding 服务(满足 `prompt.md` 的"保证可运行"要求); - 同义词表**配置化**(`mcp.router.synonyms`,映射"词 → 工具名"),便于新增工具时零改码: - 库存/存货/在库/结存/备货 → `inventory_stock_aggregate`、`inventory_sn_trace` - 发货/物流/签收 → `warehouse_list(entity=ORDER_DELIVERY)` - 欠款/未收/未付/账龄/超期 → `finance_balance_aggregate` - 项目/立项/POC/会审/报价 → `project_list` - 主数据/编码名称/客户/供应商 → `master_data_list` **接入点(向后兼容,不破坏现有客户端)** | 位置 | 行为 | |---|---| | `tools/list` | 若请求带 `params.query` → 只返回 topK;**不带则仍返回全量**(现有客户端不受影响) | | `tools/list` | 新增 `params.detail=false` → 只返回 `name` + `description`(精简 schema),需要完整 `inputSchema` 时置 `true` | | `tools/call` | 未指定 `name` 时 → 自动路由;若 top1 分数 < `min-score`(默认 0.15)→ **返回候选列表并报 `INVALID_PARAMS`**,而不是乱选 | | 新增 `tools/route` | 只做路由不做执行,便于调试与观测 | **质量要求**:每个工具的 `description` 必须含"领域词 + 业务词 + 别名"(如 `inventory_stock_aggregate` 描述需出现"库存、存货、在库、结存"),否则检索命中率上不去。 **验收**:准备 20 条中文真实问题,要求**路由命中率 ≥ 80%**,且 `tools/list` 带 `query` 时返回的 schema token 下降 ≥ 50%。 ### 16.7.1 路由配置示例(含中文 2-gram 同义词表) > 放在 `application.yml`,**新增工具只改配置、不改代码**。值支持两种写法:`tool_name` 或 `tool_name#entity`;后者会在路由结果里作为 `suggested_args` 返回(例如命中"签收" → `warehouse_list` + `entity=ORDER_DELIVERY`)。 ```yaml # ===== MCP 工具路由(RAG)配置 ===== mcp: router: enabled: true top-k: 5 # 只返回候选工具,不返回全量 min-score: 0.15 # 低于该分数不硬选:返回候选并报 INVALID_PARAMS # ---------- 检索算法参数 ---------- text: char-ngram: 2 # 中文按字符 2-gram 切分:"库存汇总" -> 库存/存汇/汇总 lowercase: true # 英文/编码统一小写(编码实际为 utf8mb4_unicode_ci,大小写不敏感) split-camel: true # 驼峰切分:inventoryStockAggregate -> inventory/stock/aggregate token-min-length: 2 weight: # 命中加权(乘到 TF-IDF 得分上) alias: 3.0 # tool-aliases 命中(人工别名,最可信) synonym: 2.5 # synonyms 命中 tool-name: 2.0 # 工具名命中 description: 1.0 # 描述 TF-IDF 基础权重 # ---------- 停止词(不参与向量化) ---------- stopwords: [的, 了, 和, 与, 及, 或者, 是, 在, 有, 我, 你, 他, 请, 帮, 帮我, 查, 查询, 看, 看一下, 多少, 几个, 哪些, 什么, 怎么, 如何, 以及, the, a, an, of, for, to, and] # ---------- 工具别名(tool -> 词,直接拼进该工具的检索文本) ---------- tool-aliases: project_order_info: [订单, 合同, 合同编号, 项目订单, 订单台账, 下单, 订单状态, 归档] product_info: [产品, 物料, 型号, 产品编码, 目录价, 指导折扣] inventory_sn_trace: [序列号, SN, 条码, 机身码, 单件, 追溯, 在库, 已出库, 入库价, 出库价] inventory_flow: [货流, 流转, 单据链, 这条货走到哪, 入出库关联, 物流轨迹, 备货状态] finance_order_position: [一单到底, 全链路, 收付票, 这单钱到哪一步, 核销情况, 计收, 毛利] inventory_stock_aggregate: [库存, 存货, 在库, 结存, 库存量, 库存汇总, 库存排行, 占用] purchase_arrival_aggregate: [采购汇总, 到货率, 到货及时率, 未入库, 在途采购, 采购金额] finance_balance_aggregate: [欠款, 未收, 未付, 余额, 账龄, 超期, 应收未收, 应付未付, 未开票, 未收票] warehouse_list: [入库单, 出库单, 发货单, 物流, 签收, 撤单, 撤回记录, 备货] purchase_list: [采购单, 采购订单, 采购明细, 供应商, 制造商, 采购变更, 采购历史] finance_list: [应收单, 应付单, 收款单, 付款单, 发票, 收票, 核销单, 计收单, 单据明细] master_data_list: [主数据, 编码转名称, 名称对照, 客户, 进货商, 代理商, 代表处, 办事处, 系统用户] project_list: [项目, 立项, 项目进度, POC, 试点, 会审, 报价, 报价单, 项目清单, 把握度] cross_domain_aggregate: [交叉分析, 透视, 组合分析, 客户产品, 按客户按产品, 按代表处, 多维] # ---------- 同义词表(用户口语 -> 工具/实体,用于加权) ---------- synonyms: # ===== 仓储 ===== 库存: [inventory_stock_aggregate, inventory_sn_trace] 存货: [inventory_stock_aggregate] 在库: [inventory_stock_aggregate, inventory_sn_trace] 结存: [inventory_stock_aggregate] 占用量: [inventory_stock_aggregate] 条码: [inventory_sn_trace] 序列号: [inventory_sn_trace] SN: [inventory_sn_trace] 扫码: [inventory_sn_trace] 入库单: [warehouse_list#INNER, inventory_flow] 到货入库: [warehouse_list#INNER, purchase_arrival_aggregate] 出库单: [warehouse_list#OUTER, inventory_flow] 发货单: [warehouse_list#DELIVERY, inventory_flow] 签收: [warehouse_list#ORDER_DELIVERY] 收货: [warehouse_list#ORDER_DELIVERY] 物流: [warehouse_list#ORDER_DELIVERY, warehouse_list#DELIVERY] 快递单号: [warehouse_list#ORDER_DELIVERY] 撤回: [warehouse_list#RECALL, warehouse_list#DELIVERY] 撤单: [warehouse_list#RECALL] 备货: [warehouse_list#STOCK, inventory_flow] 货流: [inventory_flow] 流转: [inventory_flow, warehouse_list] # ===== 采购 ===== 采购单: [purchase_list#ORDER, purchase_arrival_aggregate] 采购订单: [purchase_list#ORDER] 采购明细: [purchase_list#ITEM] 采购变更: [purchase_list#HISTORY] 采购历史: [purchase_list#HISTORY] 到货率: [purchase_arrival_aggregate] 到货及时: [purchase_arrival_aggregate] 未入库: [purchase_arrival_aggregate, purchase_list#ORDER] 在途采购: [purchase_arrival_aggregate] 供应商: [master_data_list#VENDOR, purchase_list#ORDER] 制造商: [master_data_list#VENDOR, purchase_list#ORDER] 账期: [master_data_list#VENDOR] # ===== 财务 ===== 应收: [finance_list#RECEIVABLE, finance_balance_aggregate] 应付: [finance_list#PAYABLE, finance_balance_aggregate] 收款: [finance_list#RECEIPT] 回款: [finance_list#RECEIPT, finance_balance_aggregate] 付款: [finance_list#PAYMENT] 开票: [finance_list#INVOICE] 发票: [finance_list#INVOICE] 收票: [finance_list#TICKET] 核销: [finance_list#RECEIPT, finance_list#PAYMENT, finance_order_position] 计收: [finance_list#CHARGE, finance_order_position] 毛利: [finance_order_position, finance_list#CHARGE] 欠款: [finance_balance_aggregate] 未收: [finance_balance_aggregate] 未付: [finance_balance_aggregate] 账龄: [finance_balance_aggregate] 超期: [finance_balance_aggregate] 未开票: [finance_balance_aggregate] 未收票: [finance_balance_aggregate] # ===== 项目 / 报价 ===== 项目: [project_list#PROJECT] 立项: [project_list#PROJECT] 项目进度: [project_list#PROGRESS] 把握度: [project_list#PROJECT] 试点: [project_list#POC] POC: [project_list#POC] 会审: [project_list#PROJECT, project_list#POC] 报价: [project_list#QUOTATION] 报价单: [project_list#QUOTATION] # ===== 主数据 ===== 客户: [master_data_list#CUSTOMER, master_data_list#PARTNER] 进货商: [master_data_list#PARTNER] 代理商: [master_data_list#PARTNER] 代表处: [master_data_list#AGENT] 办事处: [master_data_list#AGENT] 产品: [master_data_list#PRODUCT, product_info] 型号: [master_data_list#PRODUCT, product_info] 编码转名称: [master_data_list] 名称对照: [master_data_list] 系统用户: [master_data_list#USER] # ===== 跨域 / 组合分析 ===== 透视: [cross_domain_aggregate] 组合分析: [cross_domain_aggregate] 按客户按产品: [cross_domain_aggregate] 交叉分析: [cross_domain_aggregate] 多维: [cross_domain_aggregate] # ===== 订单(现有工具) ===== 订单: [project_order_info] 合同: [project_order_info] 合同编号: [project_order_info] # ---------- 路由回归用例(自动化验收:命中率 ≥ 80%) ---------- test-cases: - { q: 这批货走到哪了,出库了吗, expect: inventory_flow } - { q: 这个SN现在在库还是已经出库, expect: inventory_sn_trace } - { q: 某产品还有多少库存在哪个仓库, expect: inventory_stock_aggregate } - { q: 这个月有哪些采购单还没入库, expect: purchase_list#ORDER } - { q: 上个月采购到货率怎么样, expect: purchase_arrival_aggregate } - { q: 哪些订单还欠钱,账龄超过90天的, expect: finance_balance_aggregate } - { q: 这单收了多少款、开了多少票、计收了没, expect: finance_order_position } - { q: 本月开了多少发票、收了多少款, expect: finance_list#INVOICE } - { q: 这个月发货单有哪些没签收, expect: warehouse_list#ORDER_DELIVERY } - { q: 这个项目的POC到哪一步了, expect: project_list#POC } - { q: 报价单有哪些产品, expect: project_list#QUOTATION } - { q: 这些客户编码分别叫什么名字, expect: master_data_list#CUSTOMER } - { q: 按客户和产品统计每个月的销售额, expect: cross_domain_aggregate } - { q: 合同 ZGXV-20260313GDS001 的订单信息, expect: project_order_info } - { q: 9801H0BC 这个产品的目录价是多少, expect: master_data_list#PRODUCT } ``` **配置消费方式(Java 侧绑定)** ```java @Component @ConfigurationProperties(prefix = "mcp.router") public class McpRouterProperties { private boolean enabled = true; private int topK = 5; private double minScore = 0.15; private Text text = new Text(); private Map weight = new HashMap<>(); private List stopwords = new ArrayList<>(); private Map> toolAliases = new HashMap<>(); private Map> synonyms = new HashMap<>(); private List testCases = new ArrayList<>(); // getter/setter 省略;RuoYi 常用写法,与 @ConfigurationProperties 一致 } ``` **检索流程**:`query` → 去停止词 → 字符 2-gram + 英文分词 → 与「工具名 + 描述 + `tool-aliases`」的 TF-IDF 向量做余弦 → 叠加 `synonyms` 命中加权(按 `weight.synonym`)→ 取 topK;`tool#entity` 命中时在结果中附 `suggested_args`。 **冲突与兜底规则** 1. 一个词命中多个工具时按累计权重排序,取 topK(默认 5); 2. top1 分数 `< min-score` → **不硬选**,返回候选列表并报 `INVALID_PARAMS`; 3. `#entity` 只作**参数建议**,不替代 `cross_domain_aggregate` 等工具的必填参数校验(仍需工具自身护栏拦截)。 **维护约定**:新增工具时,只需在 `tool-aliases` 与 `synonyms` 各补一行,并加 1–2 条 `test-cases`;回归用例随 CI 跑,命中率跌破 80% 即告警。 ### 16.9 剩余缺口的影响评估与闭合方案(v12 分析 / v13 已全部采纳) > **v13 状态**:#1 审批待办/已办、#2 账龄分桶、#3 财务历史时点余额 **已全部纳入**(见 4.3、9 章、A.15);#4 附件元数据亦已纳入(`finance_list(entity=ATTACHMENT)`)。下表保留作为**决策依据与实现口径**。 **先纠正一处会误导的地方**:14.1 / 14.2 的 ❌ 是 **v6 快照**;v7–v11 已闭合其中 **13 项**,加上 v13 的 4 项,当前**三大域已无实质缺口**。 | # | 剩余缺口 | 不补的影响 | 闭合方案(实测) | 成本 | 建议 | |---|---|---|---|---|---| | 1 | **审批待办 / 已办**(`bu_todo` 61、`bu_todo_completed` 5,876) | 答不了"我还有哪些单要审""现在卡在谁那儿""为什么被驳回""审批耗了多久"。现有 `approve_status`/`approve_node` 只给**状态与节点**,**给不出审批人与意见** | **实测发现:两张业务表已冗余所需字段,无需碰 Flowable 的 `act_*`(2.2 万行、结构风险)**:`bu_todo` 有 `business_key`(业务主键)、`process_key`、`task_name`(节点)、`approve_user_name`(审批人)、`apply_user_name`(发起人)、`apply_time`;`bu_todo_completed` 另有 `approve_time`、**`approve_opinion`(审批意见)**、`approve_status`(**3=通过 / 2=驳回**)、`all_approve_user_name`。已覆盖流程:`order_approve_online/offline`、`purchase_order_online`、`finance_payment`、`fianance_ticket`(原文拼写)、`order_reback`、`outer_reback` —— 正好覆盖**采购、财务付款/收票、仓储撤回**。建议新增工具 **`approval_list`**(entity = `TODO` / `DONE`),按 `approve_user`(当前登录人) + `process_key` + `business_key` 过滤,游标分页 | **低**(索引:`bu_todo` 仅主键但仅 61 行;`bu_todo_completed` 有 `idx_business_key`,5,876 行全表扫亦可接受) | **建议纳入** | | 2 | **账龄分桶**(0-30 / 31-60 / 61-90 / 90+) | "超 90 天未收有多少"无法直接统计;只有 `OVERDUE_DAYS` 度量,Agent 得自己绕 | `finance_balance_aggregate` 增加 `group_by=OVERDUE_BUCKET`(基于 `plan_receipt_date` 与今天的差额分桶;仅对 `unreceived_amount > 0` 的行计数) | **零** | **建议纳入** | | 3 | **财务历史时点余额**(原运营报表能力) | 答不了"上月末未收款 vs 本月"的对比;`finance_balance_aggregate` 只给**当前值**(冗余列不支持时间点回溯) | **可按明细重算**:`应收总额(截至T) = Σ receivable_bill.total_price_with_tax where create_time ≤ T`;`已收(截至T) = Σ receipt_detail.receipt_amount where receipt_time ≤ T` → 时点未收 = 两者差。参数 `as_of_date`;**须在 metadata 标注"重算口径,与冗余列当前值口径不同"** | **中**(`receipt_detail.receipt_time` 无索引,大表需补 P2) | 视业务是否需要"历史时点" | | 4 | **财务附件** | 答不了"这笔付款的凭证/发票影像是什么" | 只提供**附件元数据**:`fileName`/`fileSize`/`fileType`/`priceWithTax`/`relatedBillType` + 关联单据;**不提供文件内容与下载**(MCP 返回文本,不做二进制传输)。实测需 **过滤 `del_flag='0'`**(80/82 有效),且 `related_bill_type` 实际值是 **`payment`(59) / `ticket`(23)**,与列注释写的 `PAYMENT_BILL_RECEIPT` 等**不符**,翻译须按实测值 | **低** | 可选 | **另有 2 项不是缺口,而是主动决策**: | 项 | 说明 | 将来若需要 | |---|---|---| | `OWNER`(负责人/销售维度) | 因无索引 + 业务价值未确认而移除 | 补 `oms_purchase_order.owner_name`、`project_order_info.duty_name` 索引后即可放开,成本低 | | 报表导出(Excel) | MCP 只返回 JSON,不产出文件 | 可先支持 `format=csv_text`(返回可粘贴的文本表格);真正的文件导出另立项 | **其余"不做"项的影响**(已在 14.8 声明):审计日志(`project_operate_log` 3,834 行)→ 答不了"谁改过这单",如需要也可低成本只读;维保入库(0 行)无影响。 **影响分级结论** | 级别 | 项 | 说明 | |---|---|---| | **影响大、建议补** | #1 审批待办/已办、#2 账龄分桶 | 分别是"业务办理"与"财务催收"的高频问法,且**成本都低**;#1 会让工具数 12 → 13(有 RAG 路由后 token 可控) | | **影响中、需业务确认** | #3 历史时点余额 | 取决于是否需要"期末对比";可按明细重算实现,但属**不同口径** | | **影响小 / 主动不做** | #4 附件元数据、`OWNER`、报表导出、审计日志 | 均为边缘场景 | ### 16.8 工具清单最终收敛(v13:13 个) | 类 | 数量 | 工具 | |---|---|---| | A 标识符点查 | 3 | `inventory_sn_trace`、`inventory_flow`、`finance_order_position` | | B 聚合(SUMMARY 默认) | 3 | `inventory_stock_aggregate`、`purchase_arrival_aggregate`、`finance_balance_aggregate` | | C 列表 / 范围 | 5 | `warehouse_list`、`purchase_list`、`finance_list`、`master_data_list`、**`approval_list`(v13)** | | D 扩展 | 2 | `project_list`、`cross_domain_aggregate` | **token 控制**:**13 个工具下,RAG 路由(16.7)从"建议"升级为"必须"**;否则每轮 schema 体积不可接受。落地顺序上,`tools/list` 的 `query` 过滤与精简描述应**与 D 类工具同期上线**。 --- ## 附录 A、字段字典(字段注释) > 说明:本附录是**字段注释的唯一权威来源**,实现时须逐字段同步到 `metadata.item_fields`(中文),写法对齐现有 `ProjectOrderInfoToolProvider#buildItemFieldMetadata()`。 > 命名约定:入参 `snake_case`,返回 `camelCase`。 > 取值翻译分两类来源,已逐字段标注:**①字典表**(`DictUtils.getDictLabel(dictType, code)`);**②Java 枚举**(`XxxEnum#getValue()`)。 > 本附录字段均取自实际 domain / Mapper XML;凡未逐值核实的一律标注"待同步",不臆造。 ### A.0 通用入参(所有分页工具共有) | 入参 | 类型 | 必填 | 中文注释 | |---|---|---|---| | `page_size` | int | 否 | 每页条数;聚合类默认 20 / 上限 200,明细类默认 20 / 上限 100 | | `cursor` | string | 否 | 上一页返回的 `next_cursor`;首页不传;与 `page` 互斥 | | `page` | int | 否 | 页码(兼容用,内部转 OFFSET,仅数据不变时稳定,不推荐) | | `include_total` | bool | 否 | 是否统计总条数;默认 false;true 时受 `count_cap=50000` 限制 | ### A.1 `inventory_sn_trace`(SN 明细·点查) **入参** | 入参 | 类型 | 必填 | 中文注释 | |---|---|---|---| | `product_sn_list` | array<string> | 三选一 | 产品序列号/条码列表,≤50(命中唯一索引 `unq_idx_sn`) | | `inner_code_list` | array<string> | 三选一 | 入库单号列表,≤20 | | `outer_code_list` | array<string> | 三选一 | 出库单号列表,≤20 | | `inventory_status` | string | 否 | 库存状态:0=在库,1=出库 | | `warehouse_id` | int | 否 | 仓库ID(低选择性,须与上述条件并用) | **返回 items**(主表 `oms_inventory_info`) | 返回字段 | 类型 | 中文注释 | 来源列 | 翻译 | |---|---|---|---|---| | `productSn` | string | 产品序列号/条码 | `product_sn` | — | | `productCode` | string | 产品BOM编码 | `product_code` | — | | `model` | string | 产品型号 | 关联 `product_info.model` | — | | `productDesc` | string | 产品描述 | 关联 `product_info.description` | — | | `inventoryStatus` | string | 库存状态编码 | `inventory_status` | — | | `inventoryStatusName` | string | 库存状态名称 | — | **枚举** `InventoryInfo.InventoryStatusEnum`:0=入库,1=出库 | | `innerCode` | string | 入库单号 | `inner_code` | — | | `outerCode` | string | 出库单号 | `outer_code` | — | | `orderCode` | string | 合同编号 | `order_code` | **注意:SN 未出库时为空**(实测为空的行数恰等于在库数量),按订单查在库货须经 `inner_code` → `oms_inventory_inner.order_code` | | `purchaseNo` | string | 采购单号 | `purchase_no` | — | | `warehouseId` | int | 仓库ID | `warehouse_id` | — | | `warehouseName` | string | 仓库名称 | 关联 `oms_warehouse_info.warehouse_name` | — | | `innerPrice` | decimal | 入库价(**含税口径待确认**,见 15.8) | `inner_price` | — | | `outerPrice` | decimal | 出库价(**含税口径待确认**,见 15.8) | `outer_price` | — | | `taxRate` | decimal | 税率(**实测存在 NULL**,未税换算按 0 兜底) | `tax_rate` | — | | `payableBillCode` | string | 对应应付单号 | `payable_bill_code` | — | | `createTime` | datetime | 创建时间 | `create_time` | — | | `updateTime` | datetime | 更新时间 | `update_time` | — | ### A.2 `inventory_flow`(单据流转链·点查) **入参**:`outer_code` 或 `order_code`(二选一,必填) **返回**:`items[0]` 为一条链,含 5 个分组: **① `inner`(入库单,`oms_inventory_inner`)** | 返回字段 | 中文注释 | 来源列 | |---|---|---| | `innerCode` | 入库单号 | `inner_code` | | `purchaseNo` | 采购单号 | `purchase_no` | | `productCode` / `productType` / `model` | 产品BOM编码 / 产品类型 / 型号 | `product_code` / `product_type` / `model` | | `quantity` | 入库数量 | `quantity` | | `vendorCode` / `vendorName` | 制造商编码 / 名称 | `vendor_code` / 关联 `oms_vendor_info` | | `warehouseId` / `warehouseName` / `warehouseType` | 仓库ID / 名称 / 类型 | `warehouse_id` / 关联 / `warehouse_type` | | `totalAmount` / `taxRate` / `taxTotal` | 入库含税总额 / 税率 / 税额 | `total_amount` / `tax_rate` / `tax_total` | | `orderCode` | 合同编号 | `order_code` | | `createTime` | 入库时间 | `create_time` | **② `outer`(出库单,`oms_inventory_outer`)** | 返回字段 | 中文注释 | 来源列 | |---|---|---| | `outerCode` | 出库单号 | `outer_code` | | `orderCode` | 合同编号 | `order_code` | | `productCode` / `model` | 产品BOM编码 / 型号 | `product_code` / `model` | | `quantity` | 应发数量 | `quantity` | | `deliveryTime` | 发货时间 | `delivery_time` | | `outerStatus` / `outerStatusName` | 出库状态编码 / 名称 | `outer_status` / **枚举** `InventoryOuter.OuterStatusEnum`:1=待确认,2=已确认,3=已接收,4=已退回 | | `deliveryStatus` / `deliveryStatusName` | 发货状态编码 / 名称 | `delivery_status` / **枚举** `InventoryOuter.DeliveryStatusEnum`:0=未发货,1=部分发货,2=全部发货,3=已撤回 | | `receivableBillCode` | 对应应收单号 | `receivable_bill_code` | | `createTime` | 创建时间 | `create_time` | **③ `outerDetails`(出库明细,`oms_inventory_outer_detail`)** | 返回字段 | 中文注释 | 来源列 | |---|---|---| | `outerCode` | 出库单号 | `outer_code` | | `warehouseId` / `warehouseName` | 仓库ID / 名称 | `warehouse_id` / 关联 | | `quantity` | 出库数量 | `quantity` | | `outerStatus` | 出库状态 | `outer_status` | **④ `deliveries`(发货单,`oms_inventory_delivery` + `_detail`)** | 返回字段 | 中文注释 | 来源列 / 翻译 | |---|---|---| | `outerCode` | 出库单号 | `outer_code` | | `warehouseId` / `warehouseName` | 仓库ID / 名称 | `warehouse_id` / 关联 | | `logisticsCompany` / `logisticsCode` | 物流公司 / 物流单号 | `logistics_company` / `logistics_code` | | `deliveryType` / `deliveryTypeName` | 发货方式 / 名称 | `delivery_type`:1=快递,2=物流,3=自提 | | `deliveryTime` | 发货时间 | `delivery_time` | | `deliveryStatus` / `deliveryStatusName` | 发货状态编码 / 名称 | `delivery_status` / **枚举** `InventoryDelivery.DeliveryStatusEnum`:0=待发货,1=已发货,2=撤回 | | `approveStatus` | 撤回审批状态 | `approve_status` | | `quantity` | 发货数量 | `quantity` | | `createByName` | 发货人 | `create_by` 关联 `sys_user` | | `detailCount` | SN 明细条数 | `oms_inventory_delivery_detail` 计数 | | `productSns` | SN 列表 | `oms_inventory_delivery_detail.product_sn` | **⑤ `snDetails`(SN 明细,`oms_inventory_info`)**:字段同 A.1。 **⑥ `stock`(备货状态,`oms_stock_info`,v7 新增)** | 返回字段 | 中文注释 | 来源列 / 翻译 | |---|---|---| | `orderCode` | 订单编码(合同编号) | `order_code` | | `stockStatus` / `stockStatusName` | 备货状态编码 / 名称 | `stock_status` / **枚举** `OmsStockInfo`:0=未备货,1=已备货 | | `onceInStock` | 是否一次备齐 | `once_in_stock` | | `createTime` | 创建时间 | `create_time` | | `deliveryTime` | 要求到货时间 | 关联 `project_order_info.delivery_time` | | `projectCode` / `projectName` | 项目编号 / 名称 | 关联 `project_order_info` → `project_info` | | `allQuantity` | 应备货总量 | 关联 `project_product_info` 汇总(非本表列) | | `notifier` / `notifierPhone` / `notifierAddress` | 通知人 / 电话 / 地址 | 关联 `project_order_info` | ### A.3 `finance_order_position`(订单财务全景·点查) **入参**:`order_code`(必填) | 分组 | 返回字段(中文注释) | 来源表 | |---|---|---| | `receivable` | `receivableBillCode`应收单号、`orderCode`合同编号、`inventoryCode`出库/入库单号、`partnerCode`/`partnerName`客户编码/名称、`productType`/`productCode`产品类型/编码、`totalPriceWithTax`含税总价、`totalPriceWithoutTax`未税总价、`taxRate`税率、`taxAmount`税额、`receivedAmount`已收款金额、`unreceivedAmount`未收款金额、`invoicedAmount`已开票金额、`uninvoicedAmount`未开票金额、`projectCode`/`projectName`项目编号/名称 | `oms_receivable_bill` | | `receiptPlans` | `planReceiptDate`计划收款日期、`planAmount`计划收款金额、`planRate`计划收款比例 | `oms_receivable_receipt_plan` | | `invoicePlans` | 计划开票日期/金额/比例(**字段名待同步** `OmsReceivableInvoicePlan`) | `oms_receivable_invoice_plan` | | `receipts` | `receiptBillCode`收款单号、`receiptStatus`/`receiptStatusName`收款状态(**枚举** `OmsReceiptBill.ReceiptStatusEnum`:-1=已退款,1=未付款,2=已付款,3=未退款)、`actualReceiptTime`实际收款时间、`totalPriceWithTax`含税金额、`writeOffAmount`核销金额、`partnerName`进货商、`receiptMethod`收款方式 | `oms_receipt_bill` | | `receiptWriteOffs` | `writeOffCode`核销单号、`writeOffType`核销方式(AUTO=自动/USER=人工)、`receiptBillCode`收款单号、`receivableBillCode`应收单号、`writeOffAmount`核销含税金额、`writeOffAmountWithoutTax`核销未税金额、`writeOffTaxAmount`核销税额、`writeOffTime`核销时间 | `oms_receivable_write_off` | | `invoices` | `invoiceBillCode`开票单号、`invoiceStatus`/`invoiceStatusName`开票状态(**枚举** `OmsInvoiceBill.InvoiceStatusEnum`:-1=已红冲,1=未开票,2=已开票,3=未红冲)、`actualInvoiceTime`实际开票时间、`invoicePriceWithTax`开票含税金额、`partnerName`客户、`approveStatus`审批状态 | `oms_invoice_bill` | | `payable` | `payableBillCode`应付单号、`orderCode`合同编号、`inventoryCode`入库/出库单号、`vendorCode`/`vendorName`制造商编码/名称、`totalPriceWithTax`含税总价、`totalPriceWithoutTax`未税总价、`taxRate`税率、`taxAmount`税额、`paidPaymentAmount`已付款金额、`unpaidPaymentAmount`未付款金额、`receivedTicketAmount`已收票金额、`unreceivedTicketAmount`未收票金额、`planPaymentDate`计划付款日期、`planTicketDate`计划收票日期 | `oms_payable_bill` | | `paymentPlans` / `ticketPlans` | `planPaymentDate`/`planAmount`/`planRate`、`planTicketDate`/`planAmount`/`planRate` | `oms_payable_payment_plan` / `oms_payable_ticket_plan` | | `payments` | `paymentBillCode`付款单号、`paymentStatus`/`paymentStatusName`付款状态(**待同步** `OmsPaymentBill.PaymentStatusEnum`)、`actualPaymentTime`实际付款时间、`totalPriceWithTax`含税金额、`writeOffAmount`核销金额、`payType`付款类型(INNER_PAY/OUTER_PAY)、`preResidueAmount`预付单剩余额度 | `oms_payment_bill` | | `tickets` | `ticketBillCode`收票单号、`ticketStatus`/`ticketStatusName`收票状态(**枚举** `OmsTicketBill.TicketStatusEnum`:-1=已红冲,1=未收票,2=已收票,3=未红冲)、`actualTicketTime`实际收票时间、`totalPriceWithTax`含税金额、`taxRate`税率 | `oms_ticket_bill` | | `paymentWriteOffs` / `ticketWriteOffs` | 同 `receiptWriteOffs` 结构(`writeOffCode`/`writeOffType`/金额三件套/`writeOffTime`) | `oms_payable_write_off` / `oms_payable_ticket_write_off` | | **`charge`(v7 新增)** | `orderCode`合同编号、`chargeStatus`/`chargeStatusName`计收状态(**枚举** `OmsFinanceCharge.ChargeStatusEnum`:0=等待收款,1=可申请计收,2=已申请计收,3=已完成计收)、`bizChargeDate`业务计收时间、`financeChargeDate`财务计收时间、`incomeWithTaxTotal`/`incomeWithoutTaxTotal`收入含税/未税、`costSoftwareWithTax`/`WithoutTax`软件成本、`costHardwareWithTax`/`WithoutTax`硬件成本、`costSoftwareMaintWithTax`/`WithoutTax`软件维保成本、`costHardwareMaintWithTax`/`WithoutTax`硬件维保成本、`costProvinceServiceWithTax`/`WithoutTax`省服务成本、`costOtherWithTax`/`WithoutTax`其他成本、`grossProfit`毛利(派生)、`grossProfitRate`毛利率(派生)、`allCostWithoutTax`成本合计(派生)、`orderChannel`下单通路、`supplier`供货商、`partnerCode`/`partnerName`进货商 | `oms_finance_charge`(`projectCode`/`projectName` 为关联字段,表内**无** `project_code` 列) | > 子列表(`receipts`/`payments`/`invoices`/`tickets`/`writeOffs`)超单页上限时返回该子列表的 `sub_cursor`,仅供该子列表翻页。 ### A.4 `inventory_stock_aggregate`(库存汇总·分页) **入参**:`product_code_list`(≤20,可选,用于收窄)、`inventory_status`(可选)、`include_warehouse_breakdown`(bool,默认 true) **返回 items**(主表 `oms_inventory_info` 聚合) | 返回字段 | 类型 | 中文注释 | 口径 | |---|---|---|---| | `productCode` | string | 产品BOM编码 | `product_code` | | `productName` | string | 产品名称 | 关联 `product_info.product_name` | | `inStockQty` | long | 在库数量 | `count(*) where inventory_status='0'` | | `outStockQty` | long | 已出库数量 | `count(*) where inventory_status='1'` | | `innerAmount` | decimal | 入库金额合计(含税) | `sum(inner_price)`,NULL 计 0 | | `outerAmount` | decimal | 出库金额合计(含税) | `sum(outer_price)`,NULL 计 0 | | `warehouses[]` | array | 仓库拆分 | 见下 | `warehouses[]` 子项:`warehouseId`仓库ID、`warehouseName`仓库名称、`inStockQty`该仓在库数量、`outStockQty`该仓已出库数量。 ### A.5 `purchase_arrival_aggregate`(采购到货汇总·分页) **入参**:`purchase_no_list`(≤20,可选)、`status`/`approve_status`/`confirm_status`(可选)、`vendor_id`(可选,**依赖 P1-2 索引**) **返回 items**(`oms_purchase_order` ⋈ `oms_purchase_order_item`) | 返回字段 | 类型 | 中文注释 | 来源 / 翻译 | |---|---|---|---| | `purchaseNo` | string | 采购单号 | `purchase_no` | | `buyerName` | string | 采购方名称 | `buyer_name` | | `vendorId` / `vendorName` | long / string | 制造商ID / 名称 | `vendor_id` / 关联 `oms_vendor_info.vendor_name` | | `warehouseId` / `warehouseName` | long / string | 入库仓库ID / 名称 | `warehouse_id` / 关联(**注意 resultMap 未映射,需工具内自行补齐**) | | `purchaserName` | string | 采购员 | `purchaser_name` | | `ownerName` | string | 汇智负责人 | `owner_name` | | `purchaseDate` | date | 采购日期 | `purchase_date` | | `status` / `statusName` | string | 采购状态编码 / 名称 | `status`:0=待入库,1=部分入库,2=已完成 | | `approveStatus` / `approveStatusName` | string | 审批状态编码 / 名称 | `approve_status`:0=草稿,1=审批中,2=已通过,3=驳回 | | `confirmStatus` / `confirmStatusName` | string | 供应商确认状态编码 / 名称 | `confirm_status`:0=待确认,1=已确认,2=已驳回 | | `payMethod` / `payMethodName` | string | 付款方式 / 名称 | `pay_method`:0=入库付款,1=出库付款 | | `flowType` | string | 线上/线下 | `flow_type`:online/offline | | `currency` | string | 币别 | `currency` | | `totalAmount` | decimal | 采购含税总金额 | `total_amount` | | `purchaseQty` | decimal | 采购数量合计 | `sum(item.quantity)` | | `innerQty` | decimal | 已入库数量合计 | `sum(item.inner_quantity)` | | `pendingQty` | decimal | 未入库数量合计 | `purchaseQty - innerQty` | | `arrivalRate` | decimal | 到货率(%) | `innerQty / purchaseQty`,HALF_UP 2 位;分母 0 返回 0 | | `amountTotal` / `taxTotal` | decimal | 明细含税金额合计 / 税额合计 | `sum(item.amount_total)` / `sum(item.tax_total)` | ### A.6 `finance_balance_aggregate`(财务余额聚合:SUMMARY 默认 / LIST 分页) **入参**:`order_code_list`(≤20,可选)、`include_zero`(bool,默认 false,是否返回全零行) **返回 items**(`oms_receivable_bill` / `oms_payable_bill` 按 `order_code` 聚合) | 返回字段 | 类型 | 中文注释 | 口径(直接 sum 冗余列,不重算) | |---|---|---|---| | `orderCode` | string | 合同编号 | `order_code` | | `projectCode` / `projectName` | string | 项目编号 / 名称 | 关联 `project_order_info` → `project_info` | | `receivableWithTax` | decimal | 应收含税总额 | `sum(total_price_with_tax)` | | `receivableWithoutTax` | decimal | 应收未税总额 | `sum(total_price_without_tax)` | | `receivableTax` | decimal | 应收税额 | `sum(tax_amount)` | | `receivedWithTax` | decimal | 已收含税金额 | `sum(received_amount)` | | `unreceivedWithTax` | decimal | 未收含税金额 | `sum(unreceived_amount)` | | `invoicedWithTax` | decimal | 已开票金额 | `sum(invoiced_amount)` | | `uninvoicedWithTax` | decimal | 未开票金额 | `sum(uninvoiced_amount)` | | `payableWithTax` | decimal | 应付含税总额 | `sum(total_price_with_tax)` | | `payableWithoutTax` | decimal | 应付未税总额 | `sum(total_price_without_tax)` | | `payableTax` | decimal | 应付税额 | `sum(tax_amount)` | | `paidWithTax` | decimal | 已付含税金额 | `sum(paid_payment_amount)` | | `unpaidWithTax` | decimal | 未付含税金额 | `sum(unpaid_payment_amount)` | | `ticketedWithTax` | decimal | 已收票金额 | `sum(received_ticket_amount)` | | `unticketedWithTax` | decimal | 未收票金额 | `sum(unreceived_ticket_amount)` | | **`overdueBucket`(v13)** | string | 账龄分桶:`0-30`/`31-60`/`61-90`/`90+` | `bucket(datediff(今天, plan_receipt_date))`,仅 `group_by=OVERDUE_BUCKET` 时返回 | | **`inBucketQty` / `inBucketAmount`(v13)** | long / decimal | 该桶内的单据数 / 未收金额合计 | 按桶聚合 | | **`asOfDate` / `basis`(v13)** | date / string | 时点日期 / 口径标记 | 传 `as_of_date` 时为 `RECALCULATED@`(按明细重算);否则 `CURRENT`(冗余列当前值) | ### A.7 `purchase_order_detail`(采购单明细·分页) > **v7 变更**:该能力已合并入 `purchase_list`,调用方式为 `entity=ORDER` + `code_list=[purchase_no...]` + `include_detail=true`。本节字段字典继续作为 `purchase_list(entity=ORDER)` 的权威来源。 **入参**:`purchase_no_list`(≤20,必填)、`status`/`approve_status`/`confirm_status`(可选) **返回 items[0](单头,`oms_purchase_order`)** `purchaseNo`采购单号、`buyerName`/`buyerAddress`采购方名称/地址、`vendorId`/`vendorCode`/`vendorName`/`vendorAddress`制造商ID/编码/名称/地址、`warehouseId`/`warehouseName`入库仓库、`purchaserName`/`purchaserMobile`采购员/手机、`ownerName`汇智负责人、`payMethod`/`payMethodName`付款方式、`currency`币别、`totalAmount`含税总金额、`taxRate`税率、`status`/`statusName`采购状态、`approveStatus`/`approveStatusName`审批状态、`approveTime`审批时间、`approveNode`当前审批节点、`confirmStatus`/`confirmStatusName`供应商确认状态、`purchaseDate`采购日期、`flowType`线上线下、`isVirtual`是否虚拟单、`version`版本号、`productCode`/`productModel`(查询条件回显)。 **返回 items[0].items(明细,`oms_purchase_order_item`)** | 返回字段 | 中文注释 | 来源列 | |---|---|---| | `purchaseId` | 采购单ID | `purchase_id` | | `productCode` / `productModel` / `productType` / `productDescription` | 产品编码 / 型号 / 类型 / 描述 | `product_code` / 关联 `product_info` | | `quantity` | 采购数量 | `quantity` | | `innerQuantity` | 已入库数量 | `inner_quantity` | | `pendingQuantity` | 未入库数量 | `quantity - inner_quantity` | | `price` | 单价 | `price` | | `taxRate` | 税率(%) | `tax_rate` | | `taxTotal` | 税额 | `tax_total` | | `amountTotal` | 含税金额 | `amount_total` | | `deliveryDate` | 交货日期 | `delivery_date` | ### A.8 `finance_bill_detail`(财务单据明细·分页) > **v7 变更**:该能力已合并入 `finance_list`,调用方式为 `entity=` + `code_list=[bill_code...]` + `include_detail=true`。本节字段字典继续作为 `finance_list` 的权威来源。 **入参**:`bill_type`(必填,枚举 `RECEIVABLE`/`PAYABLE`/`RECEIPT`/`PAYMENT`/`INVOICE`/`TICKET`)+ `bill_code_list`(≤20,必填) **单头字段(按 `bill_type` 选用)** | bill_type | 单头字段(中文注释) | 主表 | |---|---|---| | `RECEIVABLE` | 同 A.3 的 `receivable` 分组全部字段 | `oms_receivable_bill` | | `PAYABLE` | 同 A.3 的 `payable` 分组全部字段 | `oms_payable_bill` | | `RECEIPT` | 同 A.3 的 `receipts` 分组全部字段 + `receiptBillType`收款单类型、`remainingAmount`剩余金额、`receiptAccountName`/`receiptBankNumber`收款账户 | `oms_receipt_bill` | | `PAYMENT` | 同 A.3 的 `payments` 分组全部字段 + `paymentBillType`付款单类型、`paymentMethod`付款方式、`refundedAmount`/`remainingRefundAmount`退款金额、`payableBillCode`关联应付单号 | `oms_payment_bill` | | `INVOICE` | 同 A.3 的 `invoices` 分组全部字段 + `invoiceType`票据类型、`invoiceBillType`开票单类型、`partnerCode`客户编码 | `oms_invoice_bill` | | `TICKET` | 同 A.3 的 `tickets` 分组全部字段 + `ticketType`票据类型、`ticketBillType`收票单类型、`vendorCode`/`vendorName`制造商 | `oms_ticket_bill` | **明细/计划子表** | bill_type | 子表与关键字段 | |---|---| | `RECEIVABLE` | `receiptPlans`(`planReceiptDate`/`planAmount`/`planRate`)、`receiptDetails`(`receiptTime`收款时间、`receiptAmount`收款金额、`receiptRate`比例、`receiptBillCode`收款单号、`receivableDetailType`类型:1=正常收款/2=预收核销/3=退款、`receiptAmountWithoutTax`/`receiptAmountTax`)、`invoiceDetails`(`invoiceTime`/`invoiceAmount`/`invoiceRate`/`invoiceBillCode`/`receivableDetailType`:1=正常开票/3=红冲) | | `PAYABLE` | `paymentPlans`、`paymentDetails`(`paymentTime`/`paymentAmount`/`paymentRate`/`paymentBillCode`/`payableDetailType`)、`ticketPlans`、`ticketDetails`(`actualTicketTime`/`paymentAmount`/`ticketBillCode`/`paymentAmountTax`) | | `RECEIPT` | 经 `oms_receivable_receipt_detail` 关联的应收单号与核销金额;核销经 `write_off_id` → `oms_receivable_write_off` | | `PAYMENT` | 经 `oms_payable_payment_detail` 关联的应付单号与核销金额;核销经 `write_off_id` → `oms_payable_write_off` | | `INVOICE` | `oms_receivable_invoice_detail`(应收单关联)、`oms_receivable_invoice_detail_item`(开票商品行:`productCode`/`productName`/`productModel`/`quantity`/`price`/`allPrice`/`taxAmount`/`taxRate`) | | `TICKET` | 经 `oms_payable_ticket_detail` 关联的应付单号与核销金额;核销经 `write_off_id` → `oms_payable_ticket_write_off` | ### A.9 `warehouse_list`(仓储列表 / 范围查询,v7 新增) **通用入参**:`entity`(必填)、`code_list`、`status_list`、`time_range`(`begin`/`end`)、`warehouse_id_list`、`product_code_list`、`order_code`、`include_detail`、`page_size`、`cursor` | entity | 主表 | 返回字段 | |---|---|---| | `INNER` | `oms_inventory_inner` | 同 A.2① 字段 + `createByName`入库人、`remark`备注;**明细来自 `oms_inventory_info`(按 `inner_code`)**——`oms_inventory_inner_detail` 实测仅 1 行、不采用(见 4.3) | | `OUTER` | `oms_inventory_outer` | 同 A.2② 字段 + `contactPerson`/`contactPhone`/`contactAddress`、`deliveryTimeType`、`versionCode` | | `DELIVERY` | `oms_inventory_delivery` | 同 A.2④ 字段(SN 列表在 `include_detail=true` 时返回) | | **`ORDER_DELIVERY`(v8 新增)** | **`order_delivery`** | `deliveryCode`发货单号(唯一)、`orderId`关联合同ID、`deliveryDate`发货日期、`deliveryType`发货方式(1=快递,2=物流,3=自提)、`logisticsCompany`物流公司、`logisticsCode`物流单号、`receiverName`/`receiverPhone`/`receiverAddress`收货人/电话/地址、`deliveryStatus`/`deliveryStatusName`发货状态(**1=待发货,2=已发货,3=已签收**)、**`signTime`签收时间**、`remark`备注、`createdAt`/`updatedAt`;**v11 新增 join 字段(`orderId` → `order_info`)**:`orderCode`合同编号、`orderName`合同名称、`customerCode`/`customerName`客户、`orderAgentCode`代表处、`orderPartnerCode`代理商;明细 `include_detail=true` → `delivery_list` 的 `productCode`/`serialNumber`(**过滤 `deleted_at is null`**) | | `STOCK` | `oms_stock_info` | 同 A.2⑥ 字段 | | `SN` | `oms_inventory_info` | 同 A.1 字段(**必须给出索引键**,见 4.3 护栏) | | **`RECALL`(v8 新增)** | **`project_order_info_recall`** | `orderCode`合同编号、`versionCode`版本号、`operationVersion`操作版本、`createTime`更新时间、`createBy`创建人ID(撤回历史追溯) | > 提示:`order_delivery` 表存在软删除列 `deleted_at` 与 `status`(数据状态),工具内必须**默认过滤 `deleted_at is null`**;`customer_info` **没有** `partner_code` 列,不能与 `partner_info` 直接关联。 `include_detail=true` 时追加:`INNER` → `oms_inventory_inner_detail` 产品行(见 4.3);`OUTER` → `outerDetails`(同 A.2③);`DELIVERY` → `productSns`(同 A.2④);`ORDER_DELIVERY` → `delivery_list` 的 SN;`STOCK` / `SN` / `RECALL` → 无明细。 ### A.10 `purchase_list`(采购列表 / 范围查询,v7 新增) **通用入参**:`entity`(必填)、`code_list`、`status_list`、`approve_status_list`、`confirm_status_list`、`time_range`、`vendor_id`/`vendor_code_list`、`product_code_list`、`include_detail`、`page_size`、`cursor` | entity | 主表 | 返回字段 | |---|---|---| | `ORDER` | `oms_purchase_order` | 同 A.7 单头字段(`include_detail=true` 时附 A.7 明细行) | | `ITEM` | `oms_purchase_order_item` | 同 A.7 明细字段 + `purchaseNo` 采购单号 | | `ORDER_BIND` | `oms_purchase_order_map` | `orderId`订单ID、`purchaseId`采购单ID、`productCode`产品编码、`bindNum`绑定数量(**需 P2-1/P2-2 索引**) | | `HISTORY` | `oms_purchase_order_history`(+`_item_history`) | `purchaseId`原始采购单ID、`purchaseNo`、`version`版本号、`status`/`approveStatus`/`confirmStatus`(含 Name)、`totalAmount`含税金额、`vendorName`、`ownerName`、`purchaserName`、`warehouseId`、`flowType`、`isVirtual`、`createTime`/`updateTime`;明细:`productCode`/`quantity`/`price`/`taxRate`/`taxTotal`/`amountTotal`/`deliveryDate`/`innerStatus` | > **v8 变更**:原 `entity=VENDOR`(供应商主数据)已移入 `master_data_list(entity=VENDOR)`(见 A.12),避免与主数据工具重复。 ### A.11 `finance_list`(财务列表 / 范围查询,v7 新增) **通用入参**:`entity`(必填)、`code_list`、`status_list`、`approve_status_list`、`time_range`、`partner_code_list`/`vendor_code_list`、`order_code`、`include_detail`、`page_size`、`cursor` | entity | 主表 | 返回字段 | |---|---|---| | `RECEIVABLE` | `oms_receivable_bill` | 同 A.3 的 `receivable` 组 | | `PAYABLE` | `oms_payable_bill` | 同 A.3 的 `payable` 组 | | `RECEIPT` | `oms_receipt_bill` | 同 A.3 的 `receipts` 组(+ A.8 中 `RECEIPT` 的补充字段) | | `PAYMENT` | `oms_payment_bill` | 同 A.3 的 `payments` 组(+ A.8 中 `PAYMENT` 的补充字段) | | `INVOICE` | `oms_invoice_bill` | 同 A.3 的 `invoices` 组(+ A.8 中 `INVOICE` 的补充字段) | | `TICKET` | `oms_ticket_bill` | 同 A.3 的 `tickets` 组(+ A.8 中 `TICKET` 的补充字段) | | `CHARGE` | `oms_finance_charge` | 同 A.3 的 `charge` 组 | | **`ATTACHMENT`(v13 新增)** | `oms_fin_attachment`(82) | **仅元数据**:`fileName`原始文件名、`fileSize`文件大小(字节)、`fileType`MIME类型、`relatedBillId`关联单据ID、`relatedBillType`单据类型(**实测取值 `payment`(59)/`ticket`(23),与列注释不符**)、`priceWithTax`/`priceWithoutTax`附件金额、`createBy`/`createTime`、`remark`;**必须过滤 `del_flag='0'`**(80/82 有效);**不返回 `filePath` 与文件内容**(不做下载) | `include_detail=true` 时按 entity 返回对应计划 / 明细 / 核销子表(字段见 A.8 的"明细/计划子表")。 ### A.12 `master_data_list`(主数据查询 / 批量编码翻译,v8 新增) **通用入参**:`entity`(必填)、`code_list`(**批量,≤200**)、`name_like`、`status_list`、`page_size`、`cursor` | entity | 主表 | 返回字段 | |---|---|---| | `PARTNER` | `partner_info` | `partnerCode`进货商编码、`partnerName`进货商名称、`level`进货商等级(字典 `identify_level`)、`systemUserId`绑定系统用户ID | | `CUSTOMER` | `customer_info` | `customerCode`客户编码、`customerName`客户名称(**注意:本表无 `partner_code` 列,不能与 `PARTNER` 直接关联**) | | `AGENT` | `agent_info` | `agentCode`办事处编码、`agentName`办事处名称、`province`所在省、`city`所在市 | | `VENDOR` | `oms_vendor_info` | 同原 `purchase_list(entity=VENDOR)` 字段:`vendorId`/`vendorCode`/`vendorName`/`vendorAddress`/`vendorUser`/`vendorPhone`/`vendorEmail`、`vendorStatus`/`vendorStatusName`合作状态(0=正常合作,1=暂停合作)、`warehouseId`/`warehouseName`/`ownWarehouseId`、`payType`/`payConfigDay`付款方式与账期、`payName`/`payBankNumber`/`payBankOpenAddress`/`bankNumber`、`socialCredit`、`province`/`city`/`generatedAddress` | | `PRODUCT` | `product_info` | `productCode`、`productName`、`model`型号、`type`/`typeName`产品类型、`vendorCode`/`vendorName`厂商、`hzCode`、`cataloguePrice`目录价、`guidanceDiscount`指导折扣、`availableCount`可用库存、`cumulativeCount`累计出货 | | `USER` | `sys_user` | `userId`、`loginName`登录名、`userName`姓名、`deptId`/`deptName`部门、`email`、`phonenumber`手机 | | **`WAREHOUSE`(v10 新增)** | `oms_warehouse_info`(14 行) | `warehouseId`仓库ID、`warehouseCode`仓库编码、`warehouseName`仓库名称、`warehouseType`/`warehouseTypeName`仓库类型(**0=实体仓,1=虚拟仓**)、`warehouseStatus`/`warehouseStatusName`状态(**0=正常,1=停用**)、`address`详细地址、`managerName`管理员、`managerPhone`管理员电话、`managerEmail`管理员邮箱、`remark`备注。**默认只返回正常仓**(与页面 `selectOmsWarehouseInfoList` 行为一致),需含停用仓时置 `include_disabled=true`(走 `listAll`) | | **`COMPANY`(v10 新增)** | `oms_company_info`(当前 0 行) | `id`、`companyCode`公司编码、`companyName`公司名称、`companyUser`联系用户、`companyEmail`联系邮箱、`companyPhone`联系电话、`companyAddress`公司地址、`payName`账户名称、`payBankNumber`银行卡号、`payBankOpenAddress`银行开户行、`bankNumber`银行行号、`socialCredit`统一社会信用代码(**己方主体信息**,用于合同/财务场景) | > **`VENDOR` 只取 `oms_vendor_info`**:库中另有一张同名近似的 `vendor_info`(5 行,仅含 P001/P002 且字段为精简版),**经确认为历史/冗余表,明确不纳入**(已加入 15.12 排除清单)。若出现 `oms_vendor_info` 查不到的编码,按"无此厂商"处理,**不得回退查 `vendor_info`**。 **核心用途**:**批量编码 → 名称翻译**。Agent 从其他工具拿到 `partnerCode` / `vendorCode` / `productCode` 列表后,**一次调用**取回名称与属性,避免逐个查询(此前是缺口:现有 `product_info` 工具只支持模糊单值匹配)。 **索引支撑**:`partner_info.idx_partner_code`、`customer_info.idx_code(customer_code)`、`agent_info.idx_agent_code`、`product_info.uk_product_code(product_code,hz_code)`;`oms_vendor_info`(17 行)、`sys_user`(119 行) 可全表扫。按厂商过滤需 **P2-20**。 ### A.13 字典与枚举取值汇总 **① 字典表(`DictUtils.getDictLabel`)** | 字典类型 `dictType` | 用途 | 状态 | |---|---|---| | `order_status` | 订单状态 | 现有工具已在用 | | `bg_type` | BG 属性 | 现有工具已在用 | | `bg_hysy` / `bg_yys` | 行业(运营商/非运营商) | 现有工具已在用 | | `project_stage` | 项目阶段 | 现有工具已在用 | | `currency_type` | 币种 | 现有工具已在用 | | `identify_level` | 进货商类型 | 现有工具已在用 | **② Java 枚举(本轮已逐值核实)** | 枚举 | 取值 | |---|---| | `InventoryInfo.InventoryStatusEnum` | 0=入库,1=出库 | | `InventoryOuter.OuterStatusEnum` | 1=待确认,2=已确认,3=已接收,4=已退回 | | `InventoryOuter.DeliveryStatusEnum` | 0=未发货,1=部分发货,2=全部发货,3=已撤回 | | `InventoryDelivery.DeliveryStatusEnum` | 0=待发货,1=已发货,2=撤回 | | `InventoryDelivery.deliveryType` | 1=快递,2=物流,3=自提 | | `OmsWarehouseInfo.WarehouseTypeEnum` | 0=实体仓,1=虚拟仓 | | `OmsWarehouseInfo.WarehouseStatusEnum` | 0=正常,1=停用 | | `OmsPurchaseOrder` status / approveStatus / confirmStatus / payMethod / flowType | 见 A.5 逐行 | | `OmsReceiptBill.ReceiptStatusEnum` | -1=已退款,1=未付款,2=已付款,3=未退款 | | `OmsInvoiceBill.InvoiceStatusEnum` | -1=已红冲,1=未开票,2=已开票,3=未红冲 | | `OmsTicketBill.TicketStatusEnum` | -1=已红冲,1=未收票,2=已收票,3=未红冲 | | `OmsReceivableReceiptDetail.receivableDetailType` | 1=正常收款,2=预收核销,3=退款 | | `OmsReceivableInvoiceDetail.receivableDetailType` | 1=正常开票,3=红冲 | | `OmsReceivableWriteOff.writeOffType` / `OmsPayableWriteOff.writeOffType` | AUTO=自动核销,USER=人工核销 | | `OmsPaymentBill.payType` | INNER_PAY / OUTER_PAY | | `OmsStockInfo.stockStatus` | 0=未备货,1=已备货(v7 新增) | | `OmsFinanceCharge.ChargeStatusEnum` | 0=等待收款,1=可申请计收,2=已申请计收,3=已完成计收(v7 新增) | | `VendorInfo.vendorStatus` | 0=正常合作,1=暂停合作(v7 新增) | **③ 待实现时从枚举类补齐(本轮未逐值核实,不得臆造)** `OmsPaymentBill.PaymentStatusEnum`、`OmsPayablePaymentDetail.PayableDetailTypeEnum`、`OmsPayableTicketDetail.PayableDetailTypeEnum`、`OmsPayableTicketWriteOff.writeOffType`、`OmsInvoiceBill.invoiceBillType`、`OmsReceiptBill.receiptBillType`、`OmsPaymentBill.paymentBillType`、`OmsTicketBill.ticketBillType`、`OmsReceivableInvoicePlan` 字段名。 ### A.13 字段注释的落地要求 1. 每个工具的 `metadata.item_fields` 内容**必须取自本附录**,不允许遗漏或改写含义; 2. 嵌套结构(`outerDetails` / `deliveries` / `snDetails` / `warehouses` / 各 `subItems`)在 `metadata` 中以下划线分隔的扁平键声明,例如 `deliveries[].logisticsCode` = "物流单号"; 3. 枚举/字典字段一律**成对输出**:`xxx`(编码)+ `xxxName`(名称),并在 `metadata.dict_fields` 声明来源; 4. 金额字段统一 `decimal`,日期统一 `yyyy-MM-dd`,时间统一 `yyyy-MM-dd HH:mm:ss`(与现有工具一致)。 ### A.15 `approval_list`(审批待办 / 已办,v13 新增) **入参**:`entity`(必填:`TODO` / `DONE`)、`approve_user`(**默认当前登录用户ID**)、`process_key_list`、`business_key_list`、`time_range`、`page_size`、`cursor` **`entity=TODO`(`bu_todo`,61 行)** | 返回字段 | 中文注释 | 来源列 | |---|---|---| | `todoId` | 流程ID | `todo_id` | | `processInstanceId` | 流程实例ID | `process_instance_id` | | `taskId` | 任务ID | `task_id` | | `businessKey` | 业务主键(合同编号 / 采购单号等) | `business_key` | | `processKey` | 流程KEY | `process_key` | | `processName` | 流程名称 | `process_name` | | `taskName` | **当前节点名称** | `task_name` | | `approveUser` / `approveUserName` | 审批人ID / 姓名 | `approve_user` / `approve_user_name` | | `applyUserName` | 发起人姓名 | `apply_user_name` | | `applyTime` | 发起时间 | `apply_time` | | `formKey` | 节点表单KEY | `form_key` | | `extendField1/2/3` | 扩展字段 | `extend_field1/2/3` | **`entity=DONE`(`bu_todo_completed`,5,876 行)**:包含 TODO 的全部字段,另有: | 返回字段 | 中文注释 | 来源列 | |---|---|---| | `approveTime` | 审批时间 | `approve_time` | | `approveOpinion` | **审批意见** | `approve_opinion` | | `approveStatus` / `approveStatusName` | 审批结果(**3=通过,2=驳回**) | `approve_status` | | `allApproveUserName` | 所有审批人 | `all_approve_user_name` | **已覆盖的流程(实测 `process_key` 分布)** | `process_key` | 含义 | 待办 | 已办 | |---|---|---|---| | `order_approve_online` | 订单审批(线上) | 37 | 2,978 | | `order_approve_offline` | 订单审批(线下) | 6 | 1,967 | | `purchase_order_online` | **采购单审批** | 15 | 530 | | `finance_payment` | **付款审批** | 3 | 429 | | `fianance_ticket` | **收票审批** | — | 47 | | `order_reback` | **订单撤回** | — | 29 | | `outer_reback` | **出库撤回** | — | 18 | > ⚠️ **两处实测异常,勿按常规推断**:① `fianance_ticket` 是源码/数据里的**拼写错误**(少一个 `n`),匹配时**必须按原样**;② `approve_status` 的语义方向与常见枚举相反(**3 才是通过、2 是驳回**)。 --- ## 十七、变更记录 | 版本 | 主要变化 | |---|---| | **v16(补齐 15.4 与 15.11 两处"设计有、代码无")** | **① 局部游标 `sub_cursor` 落地**(此前只有 `truncated_sub_lists` 截断标注、无从续页 → 子列表超限即数据缺失):新增支持类 `McpSubPage`(协议/校验/切片/`sub_page_info`),`inventory_flow`(`outerDetails`/`snDetails`/`deliveries`)与 `finance_order_position`(`receiptPlans`/`receiptDetails`/`invoicePlans`/`paymentPlans`/`paymentDetails`/`ticketPlans`)各自实现 `sub_list` + `sub_parent` + `sub_cursor` + `page_size`;`truncated_sub_lists` 由"路径字符串"升级为 `{list, parent, total, returned, next_cursor}`,调用方可直接回传续页。**实测修正 3 处**:**(a)`max_pages` 由 20 提到 200**——20×100=2000 行 < 实测单出库单 2682 条 SN 明细,原值会导致"截断 + 游标也取不完";**(b)子列表统一在内存按主键 `id` 升序定序**——部分子表 SQL 无 `order by`,否则跨调用会重复/漏数据;**(c)修复"从 `sub_parent` 起翻时下一页游标丢父实体"缺陷**(`McpSubPage.page` 现接收已解析的 parent)。**② 查询超时 + 限流落地**:新增 `McpQueryTimeout`(线程上下文,`*_aggregate` 3s / 其余 5s)与 `McpQueryTimeoutInterceptor`(MyBatis `StatementHandler.prepare` 插件 → `setQueryTimeout`,项目自定义了 `SqlSessionFactory`,故显式 `addInterceptor`);新增 `McpRateLimiter`(**进程内**滑动窗口 60 次/分,**偏离原文的 Redis 方案,因项目未引入 Redis**);错误码新增 **`-32002 rate_limit_error`** 与 **`-32003 query_timeout`**。**实测验证**:`C-20260715001`(2682 条 SN 明细)主调 500 条 + 22 页 = **2682 条、漏 0/重复 0**;从 `sub_parent` 起翻 27 页同样取满 2682;财务 `receiptPlans` 3 页 6 条不重不漏;7 项游标错误路径(未知子列表名/缺 `sub_parent`/与 `cursor` 互斥/跨子列表串用/父实体不存在/`page_size` 超限)均返回 `-32602`;限流**第 61 次**准确触发 `-32002`;超时用"表写锁制造阻塞"实测聚合 3.3s、非聚合 5.8s 均返回 `-32003`。 | | v1 | 按表划分 10 个工具(`inventory_*` / `purchase_*` / `finance_*`),提出批量预加载、限流、字段裁剪 | | v2 | 前置验证后修正:① 否决 `oms_finance_operate_report` 物化表数据源;② 过滤键改为对齐实测索引;③ 撤销无索引支撑的时间范围过滤;④ 补库存数量聚合能力 | | v3 | ① 工具改为"面向问题",收敛为 8 个;② 引入游标分页协议与 Agent 翻页指令;③ 新增 3 个聚合工具与口径定义;④ 输出索引清单(P0/P1/P2/不可加)并完成列存在性验证;⑤ 补错误契约、权限指纹、验收指标与遗留项 | | v4 | ① 新增 3.8「与现有 MCP 工具的一致性基线」与 3.9「有意偏离(4 处)」;② 响应契约对齐现有工具(保留 `data.total`,仅新增 `page_info`,新增 `dict_fields`);③ 新增**附录 A 字段字典**:8 个工具的逐字段中文注释、来源列、枚举取值;④ 明确"字典表 vs Java 枚举"两类翻译来源,并列出待同步项,杜绝臆造 | | v5 | ① 实测证实"分析统计会退化为 Agent 驱动的多次全表扫描"(分页 34.4ms ≈ 全局汇总 38.1ms;6.5 万行 ≈ 648 页必被截断);② 新增**第十三章 分析统计场景优化**:`mode=SUMMARY` 一次算完、Top-N、覆盖索引、类型对齐防索引退化、IN 收窄、超时限流、从库路由、统计维度清单;③ 索引新增 **P1-3 覆盖索引**;④ 明确 SUMMARY 仍为 1 次 O(N) 的边界与"预聚合表需可靠定时任务"的教训 | | v6 | ① 新增**第十四章 覆盖度缺口分析与完善**:三域覆盖度矩阵、统计维度矩阵、缺口清单;② 指出**最严重缺口是"列表/范围查询整体缺失"**(所有单据只能按单号点查,"本月有哪些采购单/多少票"无法回答);③ **实测修正规则**:除 `oms_inventory_info`/`_delivery_detail` 外全部表 <1000 行,故把"无索引=不支持"改为**按表规模分级**,小表放开状态/时间/伙伴维度;④ 给出 A/B/C/D 四档完善建议;⑤ 明确"性能接近上限、覆盖不完备"的分层结论,并声明"完备 vs 轻量"的本质冲突 | | **v7** | **把 v6 识别的缺口全部并入**:① 工具由 8 → **9 个**(合并 `purchase_order_detail`/`finance_bill_detail` 进 `purchase_list`/`finance_list`,避免重复功能导致选错);② A 档落地:`group_by` 增 `STATUS`/`TIME_MONTH`/`TIME_QUARTER`/`PARTNER`/`VENDOR`,度量增 `ARRIVAL_DELAY_DAYS`/`OVERDUE_DAYS`;③ B 档落地:新增 `warehouse_list`/`purchase_list`/`finance_list` 三个 `entity` 参数化列表工具,补齐"本月有哪些/多少"类基础问题;④ C 档落地:`inventory_flow` 增 `stock` 备货分组、`finance_order_position` 增 `charge` 计收分组;⑤ D 档落地:P2 条件索引 + 时间列索引策略改为**按表规模分级**;⑥ 权限补齐到"工具 × entity"粒度对照表;⑦ 附录 A 增三个列表工具字段字典与新增枚举;⑧ 新增实施批次划分 | | **v8** | **用全库表枚举做严格比对(199 张表),修正自相矛盾之处并补齐规格空白**:① **修正 3 处自身错误**——入库明细表由"SN 明细"更正为 `oms_inventory_inner_detail`;撤销对 `oms_inventory_info.inner_price/outer_price` 的"含税"断言改为待确认;`OWNER` 维度因无索引支撑予以移除;② **新增第十五章**逐条定义 9 处规格空白(时间维度字段、维度可用性矩阵、`arrival_delay_days` 语义、`sub_cursor` 协议、`metrics` 全枚举与参数冲突规则、`include_zero` 判定、在库口径、含税口径、多币种、collation 大小写、超时/限流/埋点/schema 版本、备份表白名单、测试与压测);③ **纳入一级缺口**:`order_delivery` + `delivery_list`(manage 域发货单与 SN 明细,**补上"签收"能力**)、`project_order_info_recall`(撤回历史);④ **纳入二级缺口**:新增 `master_data_list`(**批量编码翻译**),工具数 9 → **10**;⑤ P2 索引扩至 20 条;⑥ 新增只读 SQL 扩至 16 项 | | **v9** | **补齐最后 3 类遗留**:① **覆盖缺口**——新增 `project_list`(项目/项目产品/进度/**POC**/**报价**,5 个 entity)与 `cross_domain_aggregate`(**受限跨域透视**:维度/度量白名单 + **单链路约束** + `time_range` 必填 + 分组上限,实现"客户 × 产品 × 月";真正的任意 SQL 仍不开放),工具数 10 → **12**;② **业务口径去阻塞**——在库口径与价格含税口径改为**配置项 + 工具参数 + 校验 SQL**;③ **工程项落地**——索引 DDL 执行方案、时间区间可配置策略、**从库路由实现方案**(财务类禁止走从库)、**RAG 工具路由**(`ToolEmbedding`/`ToolRetriever`/`ToolRouter` + 中文 2-gram + 同义词配置 + `tools/list` 的 `query`/`detail` 兼容扩展 + `tools/route`);④ P2 索引扩至 25 条,只读 SQL 扩至 20 项 | | **v10** | **逐表复核后收口,含 2 处实测修正**:① **实测修正 1——入库明细来源**:v8 曾改为 `oms_inventory_inner_detail`,实测该表**仅 1 行(未启用)**,而 `oms_inventory_info` 中 **579/580** 张入库单有 SN 明细 → 入库明细**以 `oms_inventory_info`(按 `inner_code`)为准**;② **实测修正 2——新增口径提醒**:`oms_inventory_info.order_code` 在 SN 未出库时为空(实测空值数 15,532 **恰等于在库数**),**不得用它反查"在库货属于哪个订单"**,须经 `inner_code` → `oms_inventory_inner.order_code`;③ **`master_data_list` 新增 2 个 entity**:`WAREHOUSE`(仓库主数据,默认只返回正常仓,`include_disabled=true` 含停用仓)与 `COMPANY`(己方公司主体 `oms_company_info`);④ **明确排除 `vendor_info`**(5 行冗余表):供应商主数据**只用 `oms_vendor_info`**,查不到不回退;⑤ 排除清单同步补入 `vendor_info` 与 `oms_inventory_inner_detail`;⑥ P2-19 因表不采用而作废 | | **v11** | **采纳方案 A:补齐 manage 域合同模型 + 3 处实测异常修正**:① **`project_list` 新增 2 个 entity**——`CONTRACT`(`order_info`,366 行,`uk_order_code(order_code,version_code)` 可直接按合同号查)与 `CONTRACT_PRODUCT`(`order_list`,854 行,`idx_order_id`);② **`warehouse_list(entity=ORDER_DELIVERY)` 补全 join**:`orderId` 实测 **355/355 指向 `order_info`**,必须 join 才能输出合同编号/客户/代理商,并明确**禁止按 id 关联 `project_order_info`**(242 条假命中);③ **明确两套订单模型关系**:manage 域与项目域**只能按 `order_code` 部分对齐(330/366)**,工具须容忍关联不到且不得臆造;④ **新增 15.14 实测异常**——`order_info.order_code` 有 **14 行前导制表符脏数据**(比对必须 `trim()`)、`order_type` 实际值为 **`zq`/`dls`**(与列注释 1/2 不符);⑤ 只读 SQL 扩至 22 项 | | **v12** | **修正"缺口矩阵误导"并评估剩余缺口**:① 14.1/14.2 矩阵补 **"现状(v11)"列**——原 ❌ 是 v6 快照,实际已被 v7–v11 闭合 **13 项**;② 新增 **16.9 剩余缺口的影响评估与闭合方案**;③ **实测推翻一处旧结论**:审批待办/已办**无需读 Flowable `act_*`**;④ 账龄分桶列为**零成本可补**;⑤ 财务历史时点余额给出"按明细重算"方案;⑥ 附件可降级为**元数据查询** | | **v15(认证态端到端验证完成)** | **用临时机器人凭证(绑定 admin,验证后已删除)对 13 个工具做真实数据 E2E,最终 34/34 用例全部通过**(含 12 个子 entity、分页不重不漏、游标串用防护、错误路径)。过程中发现并修复 **4 类实现缺陷**:① **`java.time.LocalDateTime` 序列化缺失** → `SignTime` 等字段导致 `InvalidDefinitionException`,被 `McpController` 外层 catch 吞掉后表现为 **HTTP 200 + 空响应体**;已注册 java.time 序列化器(日期 `yyyy-MM-dd`、时间 `yyyy-MM-dd HH:mm:ss`)并让该 catch **回写 error 响应**而不再静默;② **未提供的入口仍发起查询 → `where col in ()` 非法 SQL**(`inventory_sn_trace`),已改为仅对非空列表查询;③ **既有 `Transfer-Encoding` 重复响应头**(curl 丢响应体),已移除手工设置;④ **3 处 SQL 引用不存在的列**(`oms_inventory_outer.receivable_bill_code`、`oms_payable_bill.vendor_name`、`project_order_info.project_code/project_name`),已分别删除该列/改经 `oms_vendor_info` 关联/改用 `project_info`。另新增**系统性 SQL 列校验**(77 条新增 select 的 `别名.列` 与 `information_schema` 全量比对 → 0 处不存在列)。**索引实测**:P0-1/P0-2/P1-1/P1-2 **已存在**(此前巡检查漏),仅 P1-3 覆盖索引缺失,已在 `oms_test` 执行并实测 141.8ms→66.6ms(`Using index`)。 | | **v14(已实现)** | **按 v13 方案完成编码与验证**,实现期共 5 处实测修正:① **`purchase_order_map.order_id` 指向 `project_order_info`(1116/1123),不是 `order_info`**(实测 SQL 验证)——与 `order_delivery.order_id`(→`order_info`,355/355)是**两条不同链路**,方案 16.1 原表述已修正;② **MySQL `TRIM()` 不去除制表符**:`order_info.order_code` 的 14 行脏数据为前导 `\t`,必须用 `trim(replace(order_code,'\t',''))` 才能命中(15.14 补充);③ **`order_delivery.delivery_status` 实测值为 `qs`(324)/`yf`(31)**(拼音缩写),不是列注释的 1/2/3 → 翻译按实测值映射并保留数字兜底(15.14 补充);④ **`oms_receivable_bill` 无 `plan_receipt_date` 列**(账龄分桶经 `last_receipt_plan_id` 关联收款计划实现);⑤ `oms_inventory_info` **确无 `purchase_no` 列**,SN 的采购单号经 `inner_code` 关联 `oms_inventory_inner` 补齐。**验证结论**:编译通过(445 源文件);`tools/list` 返回 **15 个工具**(13 新 + 2 既有);RAG 路由 15 条中文问句 **top3 命中 100%、top1 命中 73%**,`tools/list` 带 `query` 后 schema 字节 **下降 81%**(23621→4586);无凭证调用返回 **AUTH_ERROR(-32001)** 而非空数据;`tools/call` 不传 name 可自动路由。改动规模:74 文件、+2620 行,仅 10 处删除(均为 `McpService` 的等价改写)。**框架层最小改动**:`McpService` 增加 `query`/`detail`/`tools/route`/自动路由(向后兼容),`McpController` 增加 `McpToolException` 错误码映射(原来无法返回 AUTH_ERROR)。 | | **v13(已实现)** | **三项全部纳入,三大域封版**:① **新增工具 `approval_list`**(entity = `TODO`/`DONE`,工具数 12 → **13**)——数据源 `bu_todo`(61)/`bu_todo_completed`(5,876),**不碰 Flowable `act_*`**;回答"我还有哪些单要审/卡在谁那儿/**为什么被驳回**/审批耗时";已覆盖 `order_approve_*`、`purchase_order_online`、`finance_payment`、`fianance_ticket`、`order_reback`、`outer_reback`;附录 **A.15** 给出完整字段字典与两处实测异常(`fianance_ticket` 拼写错误、`approve_status` 3=通过/2=驳回与常规相反);② **账龄分桶**:`finance_balance_aggregate` 增 `group_by=OVERDUE_BUCKET`(`0-30`/`31-60`/`61-90`/`90+`,仅 `unreceived_amount>0`);③ **财务历史时点余额**:增 `as_of_date`,按明细重算(`Σ应收 create_time≤T − Σ已收 receipt_time≤T`),并以 `metadata.basis = RECALCULATED@ \| CURRENT` 标注口径差异;④ **附件元数据**纳入 `finance_list(entity=ATTACHMENT)`(过滤 `del_flag='0'`,不返回文件内容);⑤ P2 索引增 P2-26(`bu_todo_completed(approve_user, approve_time)`)→ 编号 26 条、有效 23 条;只读 SQL 扩至 **25 项**;⑥ 15.2 维度矩阵与 15.5 冲突规则同步(`OVERDUE_BUCKET` 仅 SUMMARY) |