- 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 例)
5.1 KiB
客户换纸记录 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)
- 轨迹展开:撤纸(state=4)和上纸(state=2)两轮 ES scroll 全量拉取,Java 侧按
P_{positionsId}_{snCode}_{rfid}复合 key 合并成"纸卷轨迹";modifyTime被改写成下一卷的上纸时间、createId复用携带撤纸剩余纸长——输出契约全是隐式字段复用。 - matchQuery 分词匹配:snCode/rfid 走 matchQuery,不精确(同模块 getRfidTraceListBySncode 用 .keyword)。
- 入参 state 被强制覆盖为 2,调用方传值无效。
- scroll 深分页上限 ~10 万条截停静默丢数据。
- Redis SpecialSncode 命中的设备整组跳过(运维屏蔽逻辑)。
- ES 异常返回空壳 success,前端分不清"无数据"和"查询失败"。
- 完全无权限校验(同 Controller 其他接口都有取人,属历史遗漏)。
- 长度单位全厘米(满卷默认 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.javasrc/test/java/me/iiv/agent/es/PaperUseLogEsGatewayImplTest.java(新增 searchTraces 用例)src/test/java/me/iiv/agent/mcp/tool/DistrictPaperTraceToolsTest.javasql/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 发布。