264 lines
11 KiB
Markdown
264 lines
11 KiB
Markdown
# 库存物品分页查询 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、
|
||
无数据迁移且全程只读,不需要业务数据回滚。
|