- 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
11 KiB
设备生命周期查询 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(金蝶同步)
老接口的行为与坑:
- 行级权限:按
isagent+ agentId 过滤行(本次用户拍板:不做权限,都可以查)。 - 字段级权限:
FieldWebpageAuthority按用户角色动态裁剪返回列(MCP 场景无此概念,不复制)。 - 写死排除
exchange_state ∈ (0,1):列表里永远看不到"未生产/生产中"的设备。 按 SN 查设备的场景下这个排除会把新投产设备查成"不存在",不建议复制(见决策表)。 - 在途时长是计算字段:
roadState=1 && roadDate!=null时 roadDays = roadDate 到今天的自然日数; 超过 RedisDictionary.MaxRoadDays时 roadDaysFlag=1(列表标红用)。 - 多类型日期区间、在途天数区间等筛选项是为列表页设计的,按 SN 查询用不上,不复制。
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(刻意的简化)。
老接口的坑与对策
- 权限:仅代理角色 + 按公司过滤 → 按拍板不做权限。
- SN 不在 hardware_sncode → 老接口报错「设备编号不正确」;新工具返回
found=false, events=[]+ notice。 - 老代码多处 NPE(名称解析不判空、sendMode null 拆箱)→ 全部 null 安全,查不到为 null。
- 备用机日志 ES 异常被 catch 吞掉 → fail fast 抛 IllegalStateException(项目惯例)。
- SN 在 Redis 集合
SpecialSncode中跳过备用机日志查询(对齐老系统)。 - 发货图片 OSS 列表不返回(对 LLM 无用,刻意差异)。
- 老系统标题「生成完成」为笔误,改为「生产完成」。
- 老排序 comparator 对相等时间违反契约 → 新排序 time DESC、null 排最后,稳定排序。
- 老代码逐条 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 个 BaseMapperentity/m2:WarehouseInDetails、WarehouseOutDetails、Warehouse(仅 id/warehouseNo)mapper/m2:对应 3 个 BaseMapperes/HardwareLogEsGateway(+Impl):备用机日志查询,只注入esCloudRestClient低阶客户端 (生产 ES 7.7.1 与高阶客户端不兼容,见 EsClientConfig 注释);ES 异常抛 IllegalStateExceptionmcp/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 测试确认无回归。