Files
xiaoqu_mcp_service/xiaoqu-agent-service/doc/warehouse-stock-mcp-tool-design.md
T

264 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 库存物品分页查询 MCP 工具详细设计
> 需求:`REQ-20260915-0011`
> 工具:`warehouse.stock.list.query`
> 对齐接口:`POST /warehouseStatis/listByPage`
> 工具属性:`warehouse` / `read`
> 数据源:m2 `warehouse_stock_manage`
## 1. 背景与目标
老系统库存物品列表只能通过页面接口查询,Agent 无法在统一鉴权、工具作用域和审计体系下读取库存。
本需求将列表查询封装为只读 MCP 工具,使 Agent 能按仓库、所属公司和物品属性分页检索库存,并保持
老接口的代理商行级权限语义。
目标如下:
1. 暴露稳定、可发现的库存分页查询契约。
2. 保持总部与代理商的数据可见范围和老接口一致。
3. 返回库存业务白名单字段,并补充可用库存派生值。
4. 限制单次查询规模,避免 Agent 发起无界读取。
## 2. 范围与非目标
### 2.1 本期范围
- 新增 `warehouse.stock.list.query` MCP 工具。
- 新增 `WarehouseStockManage` 只读映射和 `WarehouseStockManageMapper`。
- 在 `PermissionService` 新增库存专用行级权限过滤。
- 支持组合筛选、分页、稳定排序、库存空值归零和可用库存计算。
- 补充工具清单、详细设计和单元测试。
### 2.2 非目标
- 不实现老系统 `/warehouseStatis/statisticsQuery` 的全量聚合;如需要,另建
`warehouse.stock.summary.query` 并单独评估权限和查询成本。
- 不新增、扣减、预占或校正库存,不修改任何业务表。
- 不返回库存变更流水、入出库单详情或 SN 级资产明细。
- 不改变老系统接口行为,也不复用其 HTTP Controller。
## 3. 总体设计
调用链如下:
```text
MCP Client
-> Token scope 校验
-> WarehouseStockTools.warehouseStockListQuery
-> 规范化分页和筛选参数
-> PermissionService.applyWarehouseStockRowFilter
-> WarehouseStockManageMapper.selectCount/selectList
-> 字段白名单映射及派生值计算
-> total/page/rows/notice/records
```
工具直接查询库存快照表,避免跨服务调用和 N+1 富化。仓库、公司、物品名称等均使用表内快照字段,
因此结果反映库存记录最近一次维护时的业务快照。
## 4. 工具契约
### 4.1 输入参数
| 参数 | 类型 | 必填 | 默认值 | 规则 | 数据库条件 |
| --- | --- | --- | --- | --- | --- |
| `warehouseIds` | `array<long>` | 否 | - | 空数组视为不筛选 | `warehouse_id IN (...)` |
| `warehouseOwnerId` | `long` | 否 | - | 精确匹配仓库所属公司 | `warehouse_owner_id = ?` |
| `materialName` | `string` | 否 | - | 去除首尾空格;空白不筛选 | `materiel_full_name LIKE` |
| `materialNo` | `string` | 否 | - | 去除首尾空格;空白不筛选 | `material_no LIKE` |
| `materialCategoryIds` | `array<long>` | 否 | - | 空数组视为不筛选 | `materiel_index_id IN (...)` |
| `specifications` | `string` | 否 | - | 去除首尾空格;空白不筛选 | `specifications LIKE` |
| `page` | `integer` | 否 | `1` | 小于 1 时归一为 1 | SQL offset |
| `rows` | `integer` | 否 | `20` | 小于 1 时归一为 20;大于 100 截断为 100 | SQL limit |
所有业务筛选条件为 AND 关系;行级权限条件作为一个括号分组追加。查询始终包含
`soft_delete = 0`。
### 4.2 返回结构
顶层字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `total` | `long` | 权限及筛选条件命中的总记录数 |
| `page` | `integer` | 归一化后的当前页码 |
| `rows` | `integer` | 实际采用的每页条数 |
| `notice` | `string` | 仅在 `rows > 100` 时返回截断提示 |
| `records` | `array<object>` | 当前页库存记录;无数据时为空数组 |
`records` 字段白名单:
| 字段 | 来源/计算 | 说明 |
| --- | --- | --- |
| `id` | `id` | 库存记录 ID |
| `warehouseId` / `warehouseNo` / `warehouseName` | 同名仓库快照列 | 仓库信息 |
| `agentId` / `agentName` | 管理公司快照列 | 库存管理公司 |
| `warehouseOwnerId` / `warehouseOwnerName` | 所属公司快照列 | 仓库所属公司 |
| `materialDefinitionId` | `materiel_definition_id` | 物料定义 ID |
| `materialNo` / `materialName` | `material_no` / `materiel_full_name` | 物品编号和名称 |
| `materialCategoryId` / `materialCategory` | `materiel_index_id` / `materiel_sort` | 分类 ID 和分类快照 |
| `specifications` / `snCode` | 同名列 | 规格和库存表 SN 快照 |
| `currentInventory` | `COALESCE(current_inventory, 0)` | 当前库存 |
| `outOfStock` | `COALESCE(out_of_stock, 0)` | 出库中/预占库存 |
| `availableInventory` | `currentInventory - outOfStock` | 可用库存;允许如实返回负数以暴露异常 |
| `batteryInventory` | `COALESCE(battery_inventory, 0)` | 电池费用化库存 |
| `updatedAt` | `modify_time` | 上海时区 `yyyy-MM-dd HH:mm:ss`;无值返回 null |
不返回 `create_id`、`create_time`、`modify_id`、`soft_delete` 等内部维护字段。
### 4.3 排序与分页
- 固定按 `current_inventory DESC, id DESC` 排序。
- `id DESC` 作为同库存量下的稳定次序,减少翻页时顺序不确定。
- offset 使用 `(page - 1) * rows`,计算过程使用 long,避免 int 乘法溢出。
- count 与列表复用同一组过滤条件;排序和 limit 仅追加到列表查询。
返回示例:
```json
{
"total": 1,
"page": 1,
"rows": 20,
"records": [
{
"id": 901,
"warehouseId": 8,
"warehouseNo": "WH-008",
"warehouseName": "一号仓",
"agentId": 10,
"agentName": "管理公司A",
"warehouseOwnerId": 10,
"warehouseOwnerName": "所属公司A",
"materialDefinitionId": 31,
"materialNo": "MAT-031",
"materialName": "擦手纸",
"materialCategoryId": 3,
"materialCategory": "纸品",
"specifications": "A型",
"snCode": null,
"currentInventory": 20,
"outOfStock": 4,
"availableInventory": 16,
"batteryInventory": 0,
"updatedAt": "2026-09-15 17:30:00"
}
]
}
```
## 5. 行级权限设计
权限对齐老系统 `WrapperUtils.getWarehouseAuth`,但在 Agent 服务中独立实现,避免依赖老系统静态工具。
| 当前身份 | 可见范围 | SQL 行过滤 |
| --- | --- | --- |
| 总部(`isagent=0`) | 全部未删除库存 | 不追加公司过滤 |
| 代理(`isagent=1`) | 本公司,以及当前用户在 `warehouse_agent_auth` 中获授权代管的公司 | `agent_id IN (ids) OR warehouse_owner_id IN (ids)` |
| 代理但 `user.agent_id` 为空 | 无 | fail closed,抛出拒绝访问异常 |
授权公司集合计算方式:
```text
visibleAgentIds = { currentUser.agentId }
visibleAgentIds += SELECT manager_agent_id
FROM warehouse_agent_auth
WHERE user_id = currentUser.id AND soft_delete = 0
```
最终条件必须保留括号:
```sql
AND (agent_id IN (:visibleAgentIds)
OR warehouse_owner_id IN (:visibleAgentIds))
```
直属下级代理不会因层级关系自动加入;只有本公司和显式 `warehouse_agent_auth` 授权公司可见。这一点
与老接口实际实现一致。MCP 层还会先校验 Token 是否绑定该工具 scope,行级权限不能替代 scope 校验。
## 6. 查询与数据处理
逻辑伪代码:
```text
pageNo = page is null or page < 1 ? 1 : page
pageSize = rows is null or rows < 1 ? 20 : min(rows, 100)
conditions = [soft_delete = 0]
append non-empty business filters
append warehouse stock row-level permission
total = count(conditions)
records = select(conditions)
order by current_inventory desc, id desc
limit offset, pageSize
map records to whitelist fields
normalize nullable inventory values to 0
calculate availableInventory
```
字符串仅在 `StringUtils.hasText` 时参与查询,并使用 MyBatis-Plus 参数绑定生成 LIKE 条件;ID 集合为空时
不生成 `IN ()`。分页 limit 只使用规范化后的整数计算结果。
## 7. 异常、边界与一致性
- 未传筛选条件:在权限范围内正常分页,不执行全量无分页返回。
- 页码超出末页:`total` 保持真实值,`records=[]`。
- `rows > 100`:成功返回,实际 `rows=100`,同时返回 `notice`。
- 库存列为 null:按 0 输出;不回写数据库。
- `availableInventory < 0`:不钳制,保留原始业务异常信号。
- 权限基础数据缺失:代理用户缺少公司 ID 时拒绝查询,不能降级为总部权限。
- 查询期间库存发生变化:count 和 list 可能反映不同瞬时快照;工具定位为运营查询,不提供事务级快照保证。
## 8. 性能与安全
- 单页上限 100,避免大结果占用数据库、MCP 序列化和模型上下文。
- 查询仅访问单表,无远程调用、循环查询或逐条富化。
- 所有筛选值由查询构造器参数化;仅 limit 使用归一化后的数值拼接。
- 固定过滤软删数据,输出采用显式白名单。
- 典型过滤列为 `warehouse_id`、`warehouse_owner_id`、`materiel_index_id`;现有生产数据量较小时无需
本需求新增索引。若慢查询监控显示退化,再依据执行计划单独提交索引变更。
## 9. 代码改动
| 模块 | 文件/组件 | 职责 |
| --- | --- | --- |
| entity | `WarehouseStockManage` | m2 库存表字段映射 |
| mapper | `WarehouseStockManageMapper` | MyBatis-Plus 单表查询 |
| tool | `WarehouseStockTools` | MCP 契约、参数处理、分页及返回映射 |
| permission | `PermissionService.applyWarehouseStockRowFilter` | 库存专用行级权限 |
| test | `WarehouseStockToolsTest` | 筛选、分页、排序、映射和 schema |
| test | `PermissionServiceWarehouseStockRowFilterTest` | 总部、代理授权和 fail-closed |
| docs | README、工具参考、本文 | 工具发现和维护说明 |
## 10. 测试与验收
### 10.1 自动化测试
1. 组合筛选生成仓库、所属公司、名称、编号、分类和规格条件。
2. `page < 1` 归一为 1,`rows > 100` 截断并产生 notice。
3. 固定排序包含 `current_inventory DESC, id DESC`,limit 正确。
4. 可用库存计算正确,电池库存 null 输出 0,时间按上海时区格式化。
5. MCP schema 发布工具名及全部输入参数。
6. 总部不追加权限过滤,也不查询代管授权。
7. 代理权限覆盖本公司和代管公司,并同时作用于管理公司、所属公司两列。
8. 代理用户缺少 `agentId` 时 fail closed。
### 10.2 验收标准
- `tools/list` 可发现 `warehouse.stock.list.query`,分类为 warehouse、类型为 read。
- 有 scope 的总部 Token 可分页查询全部未删除库存,并正确使用所有筛选条件。
- 代理 Token 不能读取权限集合之外公司的库存;授权代管后可读取对应记录。
- 返回字段、空值处理、可用库存公式、排序和分页符合第 4 节契约。
- `rows=101` 时按 100 返回并带提示;空结果返回空数组。
- 工具调用不产生库存及其他业务数据写入。
- 相关单测和项目全量 Java 测试通过。
## 11. 发布与回滚
发布后由工具同步机制注册元数据;新工具默认未绑定 Token scope,管理员需显式授权后才可调用。
上线验证依次检查 `tools/list`、总部样例查询、代理权限正反例和分页上限。
如需回滚,先解除 Token 与工具 scope 的绑定以立即停止外部调用,再回滚服务版本。由于本需求无 DDL、
无数据迁移且全程只读,不需要业务数据回滚。