5.1 KiB
5.1 KiB
设备电量消息记录 MCP 工具设计
需求:REQ-20260921(hardware.electricity.message.list.query)
背景
老系统 BatteryAnalyseController.batteryAnalyseList(xiaoqu-franchisee :63-150)消费 ES 索引
electricitymessage_yyyyMM(按月分索引,索引名 = "electricitymessage_" + 开始时间月份 yyyyMM),
文档结构 ElectricAndSignAndNetworkLogEntity:id、createTime、snCode、districtId、propertyId、
agentId、messageType(0电量/1信号/2温度/3充电循环次数/4电压/5电流)、value(六类共用 Integer)。
老接口的语义与坑:
- 只按开始时间的月份选索引,跨月范围静默丢数据;前端限制只能查一天,后端没校验。
- snCode 用 matchQuery(BaseSearchSupportImpl2 type=0 非数值分支),经 standard analyzer 分词后 AB-123 会被切成 ab/123,存在误配风险。
- messageType 默认值 0:不传参数时查询语义被默认值悄悄变成"只查电量",存在歧义。
- 接口完全无权限校验(鉴权代码被注释掉)。
- scroll 拉全量后在内存里等间隔抽样(count>300 时 i%limit>0 continue),导出接口同样无分页。
- 查询异常被 catch 吞掉,返回空结果。
设计决策(用户拍板)
| 决策 | 说明 |
|---|---|
| 不做任何权限校验 | 对齐老系统;访问控制由 MCP ak/scope 层兜底 |
| 只使用 esLocal 客户端 | 索引只建在本地集群,不走 esCloud;Gateway 只注入 esLocal bean |
| 全量分页返回 | 不做老系统的内存抽样;from+size 分页,一次请求一次查询(track_total_hits 取 total) |
| snCode 用 match_phrase | 精确短语匹配,修掉 standard analyzer 分词误配坑 |
| 单日强校验 | startTime/endTime 必须为同一天(yyyy-MM-dd 部分相等),跨天直接抛 IllegalArgumentException;startTime 不得晚于 endTime;时间格式严格 yyyy-MM-dd HH:mm:ss(ResolverStyle.STRICT) |
| 不传 messageType = 查全部类型 | 不加 term 过滤;与老系统默认值 0 的歧义语义不同,是刻意的显式设计 |
| ES 异常 fail fast | 不复制老系统 catch 吞异常返回空的语义,抛 IllegalStateException 由 GuardedToolCallback 收敛 |
参数契约
snCode:String,必填非空,match_phrase 精确短语匹配。startTime/endTime:String,必填,严格yyyy-MM-dd HH:mm:ss;两者必须为同一天,startTime ≤ endTime。messageType:Integer,可选,0-5(0电量/1信号/2温度/3充电循环次数/4电压/5电流),越界拒绝;不传=全部类型。page:Integer,默认 1(<1 归一为 1)。rows:Integer,默认 500、上限 1000;>1000 截断为 1000 并在 notice 提示(与项目现有工具惯例一致)。
索引名:electricitymessage_ + startTime 月份(yyyyMM),与老系统取法一致但跨天已被拒绝,
因此不存在跨月静默丢数据问题。
查询构造
- bool filter:match_phrase(snCode);messageType 传了才 term(messageType);range(createTime) gte/lte 闭区间。
- 排序:createTime ASC(与老系统一致)。
- track_total_hits=true,total 取精确值。
- 深分页防御:from+size 不得超 ES
max_result_window(10000)。from ≥ 10000 时截断为窗口内最后可用完整页; from < 10000 但 from+size > 10000 时截断本页条数;两种情况都在 notice 说明。
返回契约
snCode / index / total / page / rows / notice? / records[]
record: createTime / messageType / messageTypeText / value
createTime:yyyy-MM-dd HH:mm:ss;ES 文档若存 epoch 毫秒数则按 Asia/Shanghai 归一格式化。messageTypeText:服务端映射 0电量/1信号/2温度/3充电循环次数/4电压/5电流;未知/空为 null。value:原始 Integer 原样返回,不加单位标注(六类语义各自的单位老系统未定义,不做推断)。rows返回截断后的生效值;多条 notice 用";"拼接。
代码结构
es/ElectricityMessageEsGateway:接口,月索引名作为参数传入,保持可 mock;内嵌 Result/Record。es/ElectricityMessageEsGatewayImpl:只注入@Qualifier("esLocalClient");ES 异常抛 IllegalStateException。mcp/tool/ElectricityMessageTools:@XqTool(category="hardware", kind="read")+@Tool(name="hardware.electricity.message.list.query");参数校验、索引名构造、分页与窗口截断、notice 拼接、文案映射。
验证
ElectricityMessageToolsTest(mock Gateway):跨天/倒置/非法格式时间、空 snCode、非法 messageType 拒绝且零 Gateway 交互; 默认分页与 rows 截断 notice;from+size 透传;max_result_window 整页/部分截断 notice;索引月份;返回映射含 messageTypeText;Spring AI schema。ElectricityMessageEsGatewayImplTest(mock ElasticsearchClient,捕获请求构造 lambda 断言 SearchRequest): match_phrase/term 可选/range 闭区间/createTime ASC 排序/from+size/track_total_hits;命中映射含 epoch 毫秒 createTime 归一;ES 异常包装。- 全量 Maven 测试确认无回归。