- 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
169 lines
11 KiB
Markdown
169 lines
11 KiB
Markdown
# 设备生命周期查询 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 测试确认无回归。
|
||
|