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

11 KiB
Raw Blame History

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