unis_sip/docx/mcp-data-tools-plan.md

1935 lines
172 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 仓储 / 采购 / 财务 / 项目 数据类 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<McpToolProvider>` 自动注册 | **一致**:只新增工具类,**不改** `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": "<opaque>",
"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` | `<bill_code>,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 <table>` 复核本方案的"现有索引"结论(本文结论基于 `oms_test`);
2. 使用 `ALGORITHM=INPLACE, LOCK=NONE` 在线加索引,建议低峰执行;
3. 加完用 `EXPLAIN` 验证目标语句 `type` 非 `ALL`;
4. 加索引属 DDL,**不改字段、不改业务逻辑**。
---
## 七、需要新增的只读 SQL
| # | 位置 | 内容 | 目的 |
|---|---|---|---|
| 1 | `InventoryOuterDetailMapper.java/.xml` | `listByOuterCodeList(List<String>)`:`outer_code in (...)` | 消除出库明细 N+1(依赖 P0-2 索引) |
| 2 | `OmsInventoryDeliveryDetailMapper.java/.xml` | `listByDeliveryIdList(List<Long>)`:`delivery_id in (...)` | 消除发货明细 N+1(`idx_delivery_id` 已具备) |
| 3 | `InventoryInfoMapper.java/.xml` | `aggregateStock(List<String> productCodes, String groupBy, String lastKey, int limit, ...)`:按 `group_by` 动态分组 + 游标 + `order by` | `inventory_stock_aggregate` 的 SUMMARY/LIST 两种模式 |
| 4 | `InventoryInfoMapper.java/.xml` | `aggregateStockByWarehouse(List<String> 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`;如需按计收状态/时间聚合,追加 `<if>` 条件 | `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` / 财务各表 | 追加**时间范围与状态**的 `<if>` 条件(**仅小表启用**,见 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>`,且未传 `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 <table>` 复核 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@<date>` 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 <metric> 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 = "<tool>:<sub_list>"`;`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 个聚合查询的 `<choose>` 分支 + 工具参数覆盖),经确认**不引入配置机制,已全部回滚**。当前实际状态:口径**写死在 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 <table>` 复核 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 <table> ADD INDEX <idx_name> (<cols>), 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 <table> WHERE Key_name='<idx>'` + 对目标语句 `EXPLAIN` 确认 `key` 命中且 `type` 非 `ALL`;
- 回滚:`ALTER TABLE <table> DROP INDEX <idx_name>, 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<String, Double> weight = new HashMap<>();
private List<String> stopwords = new ArrayList<>();
private Map<String, List<String>> toolAliases = new HashMap<>();
private Map<String, List<String>> synonyms = new HashMap<>();
private List<TestCase> 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&lt;string&gt; | 三选一 | 产品序列号/条码列表,≤50(命中唯一索引 `unq_idx_sn`) |
| `inner_code_list` | array&lt;string&gt; | 三选一 | 入库单号列表,≤20 |
| `outer_code_list` | array&lt;string&gt; | 三选一 | 出库单号列表,≤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@<date>`(按明细重算);否则 `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=<RECEIVABLE|PAYABLE|RECEIPT|PAYMENT|INVOICE|TICKET>` + `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@<date> \| 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) |