From 8eaadbf527729ff02a283b1df411f6612ab8aeb2 Mon Sep 17 00:00:00 2001 From: my_local_kimi_dev2 Date: Mon, 12 Oct 2026 14:18:30 +0800 Subject: [PATCH] =?UTF-8?q?docs(mcp):=20=E9=87=8D=E5=86=99=E4=BA=BA?= =?UTF-8?q?=E5=9D=87=E7=94=A8=E7=BA=B8=E7=BB=9F=E8=AE=A1=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=AF=B9=E9=BD=90=E5=B7=B2=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20(REQ-20260922-0004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paper-percapita-statistics-mcp-design.md | 114 ++++++++++-------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/xiaoqu-agent-service/doc/paper-percapita-statistics-mcp-design.md b/xiaoqu-agent-service/doc/paper-percapita-statistics-mcp-design.md index 21ee4f8..f8bdf08 100644 --- a/xiaoqu-agent-service/doc/paper-percapita-statistics-mcp-design.md +++ b/xiaoqu-agent-service/doc/paper-percapita-statistics-mcp-design.md @@ -1,47 +1,51 @@ # 人均用纸统计 MCP 工具设计 -需求:REQ-20260922-0004 +来源:老系统 `StatisticsReportController`(xiaoqu-franchisee)5 个接口的 MCP 化。 -工具:`district.paper.percapita.query` - -## 1. 覆盖范围 - -一个 MCP 工具通过 `groupBy` 与 `daily` 覆盖老 `StatisticsReportController` 的五个接口: - -| 老接口 | groupBy | daily | +| 老接口 | 分组维度 | 图表形态 | |---|---|---| -| perPaperLengthStatistics | gender | false | -| perPaperLengthStatisticsDetail | gender | true | -| perPaperLengthStatisticsByFloor | floor | false | -| perPaperLengthStatisticsDetailByFloor | floor | true | -| perPaperLengthStatisticsByToilet | floorGender | false | +| perPaperLengthStatistics | gender | 柱图 | +| perPaperLengthStatisticsDetail | 日期 × gender | 折线 | +| perPaperLengthStatisticsByFloor | floor | 柱图 | +| perPaperLengthStatisticsDetailByFloor | 日期 × floor | 折线 | +| perPaperLengthStatisticsByToilet | floor × gender | 分组柱图(名为 ByToilet,实际维度是楼层×卫生间类型) | -`floorGender+daily=true` 不在老接口范围内,直接返回参数错误。 +## 1. 数据源 -## 2. 数据与口径 +- **纯 MySQL**,m0 库 `monthly_statistics` 日汇总表(Mapper 已存在:`mapper/m0/MonthlyStatisticsMapper.java`,实体 `entity/m0/MonthlyStatistics.java`)。**不查 ES**——ES 只在老系统离线写入管道里(XXL-Job 从 paperuselog 聚合落表)。 +- 聚合口径(与老系统一致):`sum(use_count)`、`sum(paper_length)/100`(表内 paper_length 单位为厘米,/100 转米),人均 = paper_length(米) / use_count,保留 2 位小数;use_count=0 时人均取 paperLength 原值(沿用老逻辑)。 +- 过滤条件:`soft_delete=0`、`statistics_time` 起止(含当天)、`district_id` 必填、可选 `device_type`、可选位置筛选 `idStrList`(`buildingId_floor_toiletId` 三段各自独立 IN)。 +- 天数上限:Redis `Dictionary` hash 的 `StatisticsDays`,超限报错(与 district.paper.statistics.query 一致)。 -- 数据源:m0 `monthly_statistics` 日汇总表,不查 ES。 -- 固定过滤:`soft_delete=0`、`district_id`、日期左闭右开区间。 -- 可选过滤:`device_type`;`idStrList` 中 building、floor、toilet 分别独立 IN。 -- `paperLengthMeters=Σpaper_length/100`,表内 `paper_length` 单位为厘米。 -- `perCapita=paperLengthMeters/Σuse_count`,四舍五入保留两位小数。 -- 当 useCount=0 时,perCapita 取 paperLengthMeters,延续老系统语义。 -- 日期上限读取 Redis `Dictionary.StatisticsDays`,缺失或非法时拒绝查询。 +## 2. 权限(与老系统的关键差异) -## 3. 参数 +老系统 5 个接口**无任何行级权限**(UserUtils 全被注释,agentId/propertyId 入参被删)。MCP 版必须补上: + +- 沿用 `district.paper.statistics.query` 的模式:先按 `permissionService.applyDistrictRowFilter` 校验 districtId 对当前用户可见,不可见抛 `IllegalStateException("站点不存在或无权访问")`。 +- 只发用户委托 Token(user_delegated),服务 Token 不给这几个 query 工具的 scope(遵守 AK/Scope 约束,见 [[xiaoqu-mcp-architecture]])。 + +## 3. 工具形态:推荐合并为 1 个工具 + +老系统 5 个接口本质是 5 种分组组合,合并成一个工具对 agent 更友好(少注册、少 scope 绑定、schema 自解释): + +``` +district.paper.percapita.query (@XqTool(category="district", kind="read")) +``` + +参数: | 参数 | 必填 | 说明 | |---|---|---| -| districtId | 是 | 正整数站点 ID | -| startDate/endDate | 是 | yyyy-MM-dd,含首尾日期,endDate 不晚于今天 | -| groupBy | 是 | gender / floor / floorGender | -| daily | 否 | 默认 false;true 时增加日期维度 | -| deviceType | 否 | 0 出纸机,1 擦手纸机 | -| idStrList | 否 | 每项严格为 buildingId_floor_toiletId | +| districtId | 是 | 站点 id | +| startDate / endDate | 是 | yyyy-MM-dd,含当天;跨度 ≤ StatisticsDays | +| groupBy | 是 | `gender` / `floor` / `floorGender`(楼层×卫生间类型,即老 ByToilet) | +| daily | 否,默认 false | true=按日折线(仅 gender/floor 支持;floorGender+daily 直接参数报错) | +| deviceType | 否 | 0=出纸机 1=擦手纸机 | +| idStrList | 否 | 位置筛选,`buildingId_floor_toiletId` 格式 | -## 4. 面向 LLM 的返回结构 +映射:`gender`+daily=false→①,+daily=true→②,`floor`+false→③,+true→④,`floorGender`→⑤。 -返回结构不随分组方式变化。维度和指标位于同一个行对象,无数据日期或分组不生成行。 +返回 `Map`(项目统一风格,无 DTO 类)。**面向 LLM 消费设计**(王恒 2026-09-22 确认:后续所有 MCP 工具设计都以返给 LLM 展示为准)——一种结构走天下,不随 groupBy/daily 变形态: ```json { @@ -53,33 +57,37 @@ "daily": false, "totalPerCapita": 1.32, "rows": [ - { - "date": null, - "floor": null, - "gender": "男卫", - "paperLengthMeters": 120.5, - "useCount": 96, - "perCapita": 1.26 - } + {"date": "2026-09-01", "floor": null, "gender": "男卫", "paperLengthMeters": 120.5, "useCount": 96, "perCapita": 1.25} ] } ``` -- 未参与分组的 `date/floor/gender` 保持为 null。 -- `daily=true` 时 `totalPerCapita` 为 null;非按日查询时为整个区间的总人均。 -- gender 映射:0男卫、1女卫、2母婴室、3无障碍、4第三卫、5其他,其他值显示未知。 -- 楼层按数据库字符串值的字典序输出。 +- `rows` 是扁平自描述行数组:每行把维度值(date/floor/gender)和指标(paperLengthMeters/useCount/perCapita)放同一对象,LLM 不需要维护平行数组下标对齐。 +- 维度字段恒在、不参与的维度为 null:`daily=false` 时 `date=null`;`groupBy=gender` 时 `floor=null`;`groupBy=floor` 时 `gender=null`;`floorGender` 两者都有值。 +- 透出 `paperLengthMeters`、`useCount` 原始值,LLM 可自行二次计算,不必从人均反推。 +- 无数据的日期/分组**不出行**(LLM 语义:没有就是没数据,不会误读成 0)——放弃老系统折线"补完整日期横轴 + 填 0.0"的做法,画折线是消费方的事。 +- `totalPerCapita` 仅在 daily=false 时给出(区间总人均);daily=true 时为 null,由消费方按需聚合。 +- gender 文案映射沿用老系统:0男卫/1女卫/2母婴室/3无障碍/4第三卫/5其他。 +- 数值统一 Double、保留 2 位小数;use_count=0 时人均取 paperLength 原值(沿用老逻辑)。 -## 5. 权限与作用域 +备选方案:1:1 拆 5 个工具(`district.paper.percapita.{gender,genderDaily,floor,floorDaily,floorGender}.query`)。更贴近老接口但注册/绑定成本 ×5,不推荐。 -- 查询前用 `PermissionService.applyDistrictRowFilter` 校验当前用户能否查看站点。 -- 站点不存在和无权访问统一返回“站点不存在或无权访问”。 -- 工具只绑定 `service_enabled=0` 的用户委托作用域,不向 sync、ops-query 等服务身份作用域开放。 +废弃方案:ECharts 平行数组结构(nameList/valueList、dateList/series、xnameList/groupList/groups)——给前端画图顺,但 LLM 消费有下标错位、补 0.0 混淆语义、多形态理解成本三个问题,已被王恒否决(2026-09-22)。 -## 6. 实现文件 +## 4. 改动文件清单 -- `src/main/java/me/iiv/agent/mcp/tool/PaperPerCapitaStatisticsTools.java` -- `src/test/java/me/iiv/agent/mcp/tool/PaperPerCapitaStatisticsToolsTest.java` -- `sql/seed-dev.sql` -- `doc/agent-mcp-tools-reference.md` -- `README.md` +1. 新建 `src/main/java/me/iiv/agent/mcp/tool/PaperPerCapitaStatisticsTools.java`(@Component implements ToolObject,骨架照抄 DistrictPaperStatisticsTools.java:73-87) +2. 实体/Mapper:**无需新增**(monthly_statistics 已覆盖) +3. 权限:**无需新增**(复用 applyDistrictRowFilter,PermissionService.java:798) +4. ES:**不涉及** +5. 单测 `src/test/java/me/iiv/agent/mcp/tool/PaperPerCapitaStatisticsToolsTest.java`(Mockito + AuthContextHolder.set(),参照 DistrictPaperStatisticsToolsTest) +6. `sql/seed-dev.sql` 加幂等 scope_tool 绑定;生产绑 scope 1/2/3/5(与 district.paper.statistics.query 同组) +7. 登记 `doc/agent-mcp-tools-reference.md` +8. 装配层零改动(McpToolConfig 自动收集 ToolObject bean,ToolSyncService 自动同步 tool 表) + +## 5. 已知坑 / 注意点 + +- 老代码 `totalPaperLength` 用 Integer 存 SQL 已 /100 的值再除,精度有损失;MCP 版统一用 Double 计算,不与老系统逐分核对。 +- `setWrapperByPosition` 的三段独立 IN 语义要保持(不是联合匹配),否则结果集不同。 +- daily 不再补完整日期横轴(老系统补 0.0 是给前端画图用;MCP 面向 LLM,无数据日期不出行)。 +- floor 是 String 列,排序按字典序(老系统行为),文档里注明。