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