Files
xiaoqu_mcp_service/xiaoqu-agent-service/doc/paper-trace-list-mcp-design.md
T
my_local_kimi_dev2 f55e07f6ec feat(mcp): 新增 district.paper.trace.list.query 客户换纸记录工具 (REQ-20261012-0002)
- ES paperuselog 热/冷索引 + LocalEsDate 云/本地集群路由(复用 statistics 同款)
- 出分页明细(from+size+track_total_hits),不复刻老系统 scroll 轨迹展开
- snCode/rfid 走 .keyword 精确匹配(修老系统 matchQuery 分词坑)
- state 可选(2上纸/4撤纸,不传查所有,修老系统强制覆盖坑);区间硬限 31 天
- applyDistrictRowFilter 站点可见性校验(老系统此接口无鉴权属历史遗漏,MCP 收紧)
- isAssignment 查 m0 rfid_info assignment_flag>=1;v1 不返回满卷标记
- 单测 420/420(新增 gateway 4 例 + 工具 9 例)
2026-10-12 14:18:30 +08:00

5.1 KiB
Raw Blame History

客户换纸记录 MCP 工具设计

需求:REQ-20261012-0002(id 5975) 工具:district.paper.trace.list.query(@XqTool(category="district", kind="read"))

背景

老系统 MonthlyStatisticsController.getRfidTraceList(/MonthlyStatistics/getRfidTraceList,客户换纸记录) 移植为 MCP 工具。数据源 ES paperuselog(热)/ paperuselog_all(冷,距今 ≥90 天),集群(云/本地)与 冷热索引路由复用 PaperUseLogEsPlanner 同款口径。

老接口行为与坑(xiaoqu-franchisee MonthlyStatisticsController.java:517-860)

  1. 轨迹展开:撤纸(state=4)和上纸(state=2)两轮 ES scroll 全量拉取,Java 侧按 P_{positionsId}_{snCode}_{rfid} 复合 key 合并成"纸卷轨迹";modifyTime 被改写成下一卷的上纸时间、 createId 复用携带撤纸剩余纸长——输出契约全是隐式字段复用。
  2. matchQuery 分词匹配:snCode/rfid 走 matchQuery,不精确(同模块 getRfidTraceListBySncode 用 .keyword)。
  3. 入参 state 被强制覆盖为 2,调用方传值无效。
  4. scroll 深分页上限 ~10 万条截停静默丢数据。
  5. Redis SpecialSncode 命中的设备整组跳过(运维屏蔽逻辑)。
  6. ES 异常返回空壳 success,前端分不清"无数据"和"查询失败"。
  7. 完全无权限校验(同 Controller 其他接口都有取人,属历史遗漏)。
  8. 长度单位全厘米(满卷默认 18000cm=180m);时间校验异常被吞,传空时间直接 500。

决策表(2026-10-12 王恒拍板)

# 决策点 结论
1 轨迹 vs 明细 出分页明细,不复刻轨迹展开。scroll 全量 + 隐式字段改写分页不可做、契约隐式、静默丢数据;agent 拿到按 rfid 分组的明细(createTime 升序)可自行重构"每卷纸用了多久"
2 时间上限 硬上限 31 天(startDate/endDate 必填,yyyy-MM-dd 闭区间日粒度;不依赖 Redis StatisticsDays)
3 满卷标记 newPaperFlag v1 不返回,只返回原始 lengthCm 由 agent 判断(老系统需查 paper_info.length 按站点纸规格判定;v1.1 再评估)

参数契约

参数 必填 说明
districtId 是 站点 id,正整数;先查 district_strategy 校验可见性
startDate / endDate 是 yyyy-MM-dd,闭区间,endDate 不得晚于今天;区间 ≤31 天
snCode / rfid 否 keyword 精确匹配(修老系统 match 分词坑)
state 否 2=上纸 / 4=撤纸,不传=查所有(修老系统强制覆盖坑)
deviceType 否 0=出纸机 / 1=擦手纸机
page / rows 否 默认 1/20,rows 上限 100 截断 + notice(标准约定)

返回契约

districtId / districtName / index / total / page / rows / [notice] / records,按 createTime ASC 排序。 record 含 createTime、state+stateText(上纸/撤纸)、rfid、snCode、lengthCm(原始长度,字段名带单位,厘米)、 paperlengthCm、positionsId(0=厕所级)、toiletId、gender+genderText、deviceType、 isAssignment(m0 rfid_info 该 rfid assignment_flag≥1 为 true)。

与老系统的差异

  • 权限:老系统裸奔 → MCP 复用 district.paper.statistics.query 同款 applyDistrictRowFilter, 不可见抛"站点不存在或无权访问",不泄露存在性。
  • 明确不搬:SpecialSncode 屏蔽(运维专用)、scroll 深分页(改标准 from+size)、空壳 success (MCP 异常直接抛 IllegalStateException)、同 Controller 的无鉴权运维 GET 接口(testsetMonthlyStatics*/ update*/reset* 绝不暴露)。
  • endDate 上界口径与 statistics 工具同款:endDate=今天 → 取今天 00:00-1ms(当天数据未跑完),否则次日 00:00-1ms。
  • ES 查询一律低阶 RestHighLevelClient(高阶 elasticsearch-java 被生产 ES 7.7.1 双重拒绝,见 electricitymessage 坑沉淀); createTime range 发 epoch 毫秒;_source 文本值归一。

实现文件

  • src/main/java/me/iiv/agent/es/PaperUseLogEsGateway.java + Impl:新增 searchTraces 明细查询 (term districtId、snCode.keyword/rfid.keyword term、state/deviceType 可选、from+size、track_total_hits)
  • src/main/java/me/iiv/agent/entity/m0/RfidInfo.java + mapper/m0/RfidInfoMapper.java:极简实体(isAssignment 判定)
  • src/main/java/me/iiv/agent/mcp/tool/DistrictPaperTraceTools.java
  • src/test/java/me/iiv/agent/es/PaperUseLogEsGatewayImplTest.java(新增 searchTraces 用例)
  • src/test/java/me/iiv/agent/mcp/tool/DistrictPaperTraceToolsTest.java
  • sql/seed-dev.sql 幂等 scope_tool 绑定;生产绑 scope 1(test)/2(operation)/3(xiaoqu-ai)/5(ops-query)

测试要点

参数校验(在任一查询前全部拦截)/ 权限(不可见站点、applyDistrictRowFilter 必调)/ 字典缺失 fail fast / LocalEsDate 跨界拒绝 / 冷热索引与集群路由 / 枚举映射(stateText、genderText 未知为 null)/ rows 截断 notice / endDate 上界毫秒口径 / 空记录跳过 rfid_info 查询 / Spring AI schema 发布。