Files
xiaoqu_mcp_service/xiaoqu-agent-service/doc/device-lifecycle-mcp-tool-design.md
T
wangheng 1f145e0c57 feat(mcp): 新增设备生命周期查询工具
- hardware.device.lifecycle.query:按 SN 模糊分页查询 device_life_cycle 当前状态,
  含流转/在途/在库/联网/资产五组状态文案、关键日期、在途天数与超期标记
  (阈值取 Redis Dictionary.MaxRoadDays,缺失 fail fast)
- hardware.device.lifecycle.detail.query:按 SN 精确聚合事件时间线,
  覆盖生产档案/质检/出入库(含发货信息)/安装上下线/备用机日志(ES hardwarelog),
  时间倒序,SN 不存在返回 found=false
- 复刻老系统 lifeCycleListByPage 与 lookDetail,按拍板不做权限校验、
  不复制 exchange_state 排除、物流公司名用 mall_order_logistics 冗余列
- ES 备用机日志走低阶 RestClient 云集群,SpecialSncode 集合命中跳过
- 新增 15 个单测(含 ES 请求体断言、排序、null 安全、fail fast 路径)
- 设计文档:doc/device-lifecycle-mcp-tool-design.md
2026-09-24 09:40:06 +08:00

169 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 工具设计
需求:设备生命周期按 SN 查询(列表 + 详情两个工具),2026-09-23 用户拍板「按建议来」
工具一:`hardware.device.lifecycle.query`(当前状态,列表)
工具二:`hardware.device.lifecycle.detail.query`(事件时间线,详情)
## 背景
老系统 `LifeCycleController.lifeCycleListByPage`(xiaoqu-franchisee :63-161)分页查询
`device_life_cycle` 表(**m0 库**,mapper 在 `mapper.m0`,与 battery_fault_log 同库),
实体 `beans.b0.DeviceLifeCycle`(xiaoqu-publicbeanandmap)。
表字段(23 个业务字段):
- 标识:`sn_code`、`device_type`(0出纸机/1空感/2电池/3充电箱/4泡沫机/5蓝牙网关)
- 状态组:`exchange_state`(0未生产/1生产中/2生产完成/3出厂/4报废/5自用/6出库/7入库/8安装/9上线/10下线/11备用/12解除备用/13重工)、
`up_exchange_state`(上次流转状态)、`road_state`(-1初始化/0落地/1在途)、
`inventory_state`(-1初始化/0非在库/1在库)、`network_state`(0离线/1在线)、
`asset_state`(0库存商品/1固定资产/2出租/3售出/4报废/5费用化)
- 日期组:`producing_date`、`produce_finish_date`、`ex_factory_date`、`first_use_date`、
`discard_date`、`again_produce_date`、`road_date`(在途开始时间)、`last_online_date`
- 归属:`agent_id`/`agent_name`(资产使用公司)、`asset_owner_id`/`asset_owner_name`(资产所属公司)、`destination`(去向)
- 其他:`last_op_user`/`last_op_date`(最近流转人与时间)、`last_latitude_and_longitude`(最近经纬度)、
`jindie_sync_state`/`jindie_internal_id`(金蝶同步)
老接口的行为与坑:
1. **行级权限**:按 `isagent` + agentId 过滤行(本次用户拍板:不做权限,都可以查)。
2. **字段级权限**:`FieldWebpageAuthority` 按用户角色动态裁剪返回列(MCP 场景无此概念,不复制)。
3. **写死排除 `exchange_state ∈ (0,1)`**:列表里永远看不到"未生产/生产中"的设备。
按 SN 查设备的场景下这个排除会把新投产设备查成"不存在",**不建议复制**(见决策表)。
4. **在途时长是计算字段**:`roadState=1 && roadDate!=null` 时 roadDays = roadDate 到今天的自然日数;
超过 Redis `Dictionary.MaxRoadDays` 时 roadDaysFlag=1(列表标红用)。
5. 多类型日期区间、在途天数区间等筛选项是为列表页设计的,按 SN 查询用不上,不复制。
6. `WrapperUtils.beanToWrapper` 对字符串列是 like 模糊匹配(snCode 实际为模糊查询)。
## 设计决策(用户已拍板 2026-09-23)
| # | 决策 | 说明 |
|---|---|---|
| 1 | 不做任何权限校验 | 用户拍板:都可以查;访问控制由 MCP ak/scope 层兜底(与 electricity.message 一致) |
| 2 | 不复制 exchange_state 排除 | 老系统写死排除未生产/生产中,按 SN 精确查会误报"不存在";新工具全状态可见。**若要严格对齐老列表口径请告知** |
| 3 | snCode 模糊匹配 | 对齐老系统 like 语义:传完整 SN 返回唯一一条;传片段返回多条分页列表 |
| 4 | 保留在途时长计算 | roadDays / roadDaysFlag 是老列表的核心展示字段,服务端算好返回;Redis `Dictionary.MaxRoadDays` 缺失时 fail fast(与 paper 系列工具一致) |
| 5 | 枚举全部配文案映射 | deviceType/exchangeState/roadState/inventoryState/networkState/assetState/jindieSyncState 各配 `*Text` 字段,LLM 友好 |
| 6 | 金蝶字段原样返回 | jindieSyncState/jindieInternalId 保留,排查资产同步问题时有用 |
## 参数契约
- `snCode`:String,必填非空;like 模糊匹配(对齐老系统),命中唯一即单设备详情语义。
- `page`:Integer,默认 1(<1 归一为 1)。
- `rows`:Integer,默认 20、上限 100;>100 截断为 100 并在 notice 提示(与项目现有工具惯例一致)。
固定条件:`soft_delete=0`,按 `id ASC` 稳定排序(对齐老系统)。
## 返回契约
```
total / page / rows / notice? / records[]
record:
snCode / deviceType / deviceTypeText
exchangeState / exchangeStateText / upExchangeState / upExchangeStateText
roadState / roadStateText / roadDate / roadDays / roadDaysFlag
inventoryState / inventoryStateText / networkState / networkStateText
assetState / assetStateText
destination / agentId / agentName / assetOwnerId / assetOwnerName
producingDate / produceFinishDate / exFactoryDate / firstUseDate / discardDate / againProduceDate
lastOpUser / lastOpDate / lastOnlineDate / lastLatitudeAndLongitude
jindieSyncState / jindieSyncStateText / jindieInternalId
createTime / modifyTime
```
- 所有日期:`yyyy-MM-dd HH:mm:ss`(Asia/Shanghai),null 原样为 null。
- `roadDays`:仅 `roadState=1 && roadDate!=null` 时返回自然日数,否则 null;`roadDaysFlag` 默认 0,
roadDays > `MaxRoadDays` 时为 1。
- 枚举值遇到未知 code:`*Text` 返回 `未知(code)`,原 code 照常返回(与 device_average 工具一致)。
- `upExchangeState` 为 null 时 `upExchangeStateText` 为 null。
## 代码结构
- `entity/m0/DeviceLifeCycle`:MyBatis-Plus 实体,映射 `device_life_cycle` 全字段。
- `mapper/m0/DeviceLifeCycleMapper`:BaseMapper,无需自定义 SQL(条件简单,LambdaQueryWrapper 即可)。
- `mcp/tool/DeviceLifecycleTools`:`@XqTool(category="hardware", kind="read")` +
`@Tool(name="hardware.device.lifecycle.query")`;参数校验、枚举文案映射、在途时长计算、notice 拼接。
无权限校验、无 ES、无跨库 join,单表分页查询。
## 验证
- `DeviceLifecycleToolsTest`(mock Mapper):空 snCode 拒绝且零 Mapper 交互;rows 上限截断 notice;
soft_delete/id ASC 条件透传;枚举文案映射(含未知 code 与 null);roadDays/roadDaysFlag 计算
(在途未超期/超期/非在途三种);MaxRoadDays 字典缺失 fail fast;返回字段完整性;Spring AI schema。
- 全量 Maven 测试确认无回归。
## 工具二:hardware.device.lifecycle.detail.query(详情时间线)
老系统 `AssetDepreciationSetController.lookDetail`(xiaoqu-franchisee :141-418)按 SN 聚合 6 个数据源,
返回按操作时间倒序的生命周期事件列表(`AssetDetailsOutput`:title + 中文标签 LinkedHashMap)。
### 事件来源(全部 soft_delete=0)
| 事件 type | 标题 | 来源 | 库 | 时间字段 | 字段 |
|---|---|---|---|---|---|
| unproduced | 未生产 | hardware_sncode.create_time | m0 | createTime | orderNo/orderPack/materialNo/materielFullName/nfcBound/sysversion/hardVersion/memo/operator |
| producing | 生产中 | hardware_sncode.unbing_time(null 则不产生) | m0 | unbingTime | operator |
| produced | 生产完成 | hardware_check_record limit 1 | m0 | modifyTime | operator |
| inbound | 入库 | warehouse_in_details(order_state=3) | m2 | inConfirmTime | inOrderNo/warehouseTypeName/applyUserName/applyTime/warehouseName/inConfirmUserName/inDate |
| rework | 重工 | 同上,仓库号=1000000998 | m2 | 同上 | 同上 |
| outbound | 出库 | warehouse_out_details(order_state=3) | m2 | outConfirmTime | outOrderNo/warehouseTypeName/applyUserName/applyTime/warehouseName/useCustomerName/outConfirmUserName/outDate + shipping |
| exFactory | 出厂 | 同上,仓库号=BJ001 | m2 | 同上 | 同上 |
| install | 安装 | hardware_install_log(logType=0) | m0 | onlineTime | agentName/propertyName/districtName/address/operator |
| online | 上线 | 同上(logType=1) | m0 | 同上 | 同上 |
| offline | 下线 | 同上(logType=2) | m0 | 同上 | 同上 |
| backup | 备用 | ES hardwarelog(term snCode + term logType=备用机操作日志,id DESC,size 1000) | 云集群 | createTime | agentName/propertyName/districtName/event/operator(userName) |
发货信息 shipping(出库/出厂事件内嵌):优先 mall_order_no、否则 out_order_no 查
m1 `mall_order_logistics`(soft_delete=0);sendMode=0 → {mode:"快递", logisticsName, logisticsOrderNo},
sendMode=1 → {mode:"自行派送", deliverName},其余/查不到不返回 shipping。
物流公司名直接用 mall_order_logistics.logistics_name 冗余列,不再 join mall_logistics(刻意的简化)。
### 老接口的坑与对策
1. 权限:仅代理角色 + 按公司过滤 → 按拍板不做权限。
2. SN 不在 hardware_sncode → 老接口报错「设备编号不正确」;新工具返回 `found=false, events=[]` + notice。
3. 老代码多处 NPE(名称解析不判空、sendMode null 拆箱)→ 全部 null 安全,查不到为 null。
4. 备用机日志 ES 异常被 catch 吞掉 → fail fast 抛 IllegalStateException(项目惯例)。
5. SN 在 Redis 集合 `SpecialSncode` 中跳过备用机日志查询(对齐老系统)。
6. 发货图片 OSS 列表不返回(对 LLM 无用,刻意差异)。
7. 老系统标题「生成完成」为笔误,改为「生产完成」。
8. 老排序 comparator 对相等时间违反契约 → 新排序 time DESC、null 排最后,稳定排序。
9. 老代码逐条 selectById 解析名称(N+1)→ 批量收集 id 后 selectBatchIds 一次解析。
### 参数契约
- `snCode`:String,必填非空,自动 trim;精确匹配(详情语义就是单台设备)。
### 返回契约
```
snCode / found / notice? / total / events[]
event: type / title / time / fields{...}
```
- `time`:yyyy-MM-dd HH:mm:ss(Asia/Shanghai);入库/出库的 inDate/outDate 为 yyyy-MM-dd。
- `events` 按 time 倒序,null 时间排最后。
## 代码结构
- `entity/m0`:DeviceLifeCycle、HardwareSncode、HardwareInstallLog、HardwareCheckRecord、District(仅 id/districtName)
- `mapper/m0`:对应 5 个 BaseMapper
- `entity/m2`:WarehouseInDetails、WarehouseOutDetails、Warehouse(仅 id/warehouseNo)
- `mapper/m2`:对应 3 个 BaseMapper
- `es/HardwareLogEsGateway(+Impl)`:备用机日志查询,只注入 `esCloudRestClient` 低阶客户端
(生产 ES 7.7.1 与高阶客户端不兼容,见 EsClientConfig 注释);ES 异常抛 IllegalStateException
- `mcp/tool/DeviceLifecycleTools`:两个 @Tool 方法,参数校验、枚举文案映射、在途时长计算、
事件聚合与名称批量解析、notice 拼接
## 验证
- `DeviceLifecycleToolsTest`(mock 各 Mapper + Gateway + Redis):
列表——空 snCode 拒绝、rows 截断 notice、枚举映射、roadDays/roadDaysFlag、MaxRoadDays 缺失 fail fast;
详情——SN 不存在返回 found=false、各事件类型聚合与字段映射、出厂/重工仓库号特判、
发货信息两种 sendMode、SpecialSncode 跳过 ES、名称解析 null 安全、时间倒序、Spring AI schema。
- `HardwareLogEsGatewayImplTest`(mock RestClient):term 过滤、id DESC、size 1000、
createTime epoch 毫秒归一、ES 异常包装。
- 全量 Maven 测试确认无回归。