- district.traffic.statistics.query:复刻老 trafficStatistics 饼图与 trafficStatisticsDetail 折线图,SUM(use_count) 按 gender/日期×gender 聚合, view=gender 占比 + view=daily_gender 按日趋势零填充(修正老系统插入错位 bug) - 行级权限对齐 district.paper.* 系列;gender=null 归「未配置」不再 NPE - 日期区间硬限 31 天(一个月),超出报错提示修改日期(2026-09-24 拍板); district.paper.percapita.query 与 district.paper.device_average.query 同步加限, 与 Redis StatisticsDays 取更严 - 新增 12 个单测;设计文档 doc/traffic-statistics-mcp-tool-design.md
112 lines
7.0 KiB
Markdown
112 lines
7.0 KiB
Markdown
# 人流量统计 MCP 工具设计(评审稿)
|
||
|
||
需求:老系统人流量统计两个接口做成 MCP 工具,2026-09-24 用户截图指派(trafficStatistics 饼图 + trafficStatisticsDetail 折线图),先评审再实现。
|
||
|
||
拟建工具:`district.traffic.statistics.query`(**一个工具、两个 view**,对齐
|
||
`district.paper.device_average.query` 把老系统多接口合并为 view 参数的既有风格)
|
||
|
||
- `view=gender`:人流量占比(复刻 `/statisticsReport/trafficStatistics` 饼图)
|
||
- `view=daily_gender`:人流量按日趋势(复刻 `/statisticsReport/trafficStatisticsDetail` 折线图)
|
||
|
||
## 背景
|
||
|
||
老系统 `StatisticsReportController`(xiaoqu-franchisee):
|
||
|
||
- `trafficStatistics`(:347-442):饼图,按 gender 分组 SUM(use_count),算占比
|
||
- `trafficStatisticsDetail`(:453-559):折线图,按 日期×gender 分组 SUM(use_count),区间内日期零填充
|
||
|
||
两个接口查询的都是 **m0 `monthly_statistics`** 日汇总表(与 district.paper.* 系列同一张表),
|
||
过滤条件完全一致:`statistics_time` 区间(start 00:00:00 ~ end 23:59:59)+ `district_id` 必填
|
||
+ `setWrapperByInput`(:3062-3070)可选 `idStrList`(`楼id_楼层_厕所id` 拼串,拆成三个 IN)与 `device_type`。
|
||
天数上限走 Redis `Dictionary.StatisticsDays`。
|
||
|
||
老接口的行为与坑:
|
||
|
||
1. **权限校验是注释掉的**(`// User user = UserUtils.getUser(request)`),裸奔。
|
||
2. **`use_count` 即人流量**,与用纸长度无关——现有 `district.paper.statistics.query` 虽也返回 useCount,
|
||
但受厕位/厕墙口径约束(厕墙行恒 0,老 :200-203 意图),不能直接当人流量用,所以单独建工具。
|
||
3. **折线零填充有 bug**(:518-525):首次出现的 gender 用 `curValueList.add(dayOrder, v)` 是**插入**而非覆盖
|
||
(valueList 预填充时又漏了下标 0),序列与日期错位;后续 `set(dayOrder, ...)` 才是对的。新工具修正为
|
||
按区间长度预填 0 再 set。
|
||
4. **gender 为 null 直接 NPE**(:387 `gender.toString()`),老数据有 null 时会 500。新工具归为「未配置」分组。
|
||
5. percent 用 `DecimalFormat("######0.00")` 截断两位;totalCount=0 时 percent=0。
|
||
6. 天数上限校验被 try/catch 吞掉(:366-368),Redis 缺 StatisticsDays 时静默放行。新工具 fail fast
|
||
(与 paper 系列一致)。
|
||
7. idStrList 拼串(`123_F1_456`)是前端树组件的传参方式,MCP 场景语义差,
|
||
建议改为与 `district.paper.device_average.query` 一致的 `buildingIds/floors/toiletIds` 三个列表 IN。
|
||
|
||
## 待拍板决策
|
||
|
||
**2026-09-24 用户拍板:全部按建议执行(决策 1-7 锁定),并追加两条:**
|
||
|
||
- **决策 8(追加):日期区间硬限一个月(31 天)**,超出直接报错「查询时间段最长 31 天(一个月),请修改日期范围」。
|
||
本工具**不依赖** Redis `Dictionary.StatisticsDays`(硬限即上限,避免双口径)。
|
||
- **决策 9(追加):存量工具 `district.paper.percapita.query`(33)与 `district.paper.device_average.query`(34)同步加 31 天硬限**,
|
||
原有 StatisticsDays 检查保留(两者取更严)。
|
||
|
||
| # | 决策点 | 建议 | 备选 |
|
||
|---|---|---|---|
|
||
| 1 | 工具拆分 | **一个工具两个 view**(对齐 34 号工具合并风格) | 两个工具 1:1 对齐老接口 |
|
||
| 2 | 权限 | **做行级权限**(`applyDistrictRowFilter`,与同表的 district.paper.* 一致,不可见返回「站点不存在或无权访问」) | 对齐老系统裸奔(权限已注释) |
|
||
| 3 | 位置过滤入参 | **buildingIds/floors/toiletIds 三列表 IN**(对齐 34 号工具) | 复制老 idStrList 拼串 |
|
||
| 4 | deviceType 取值 | **不限枚举**,任意非负整数透传(老系统无约束;人流量不限于出纸设备) | 限制 0/1(与纸工具一致) |
|
||
| 5 | endDate 晚于今天 | **拒绝**(对齐新工具族;monthly_statistics 是日汇总,查未来无意义) | 老系统不限制,放行 |
|
||
| 6 | gender=null | 归为 `genderCode=null, gender="未配置"` 分组 | 跳过该行(老系统 NPE) |
|
||
| 7 | 分类 | **customer**(按「分类都放到 customer」口径) | hardware |
|
||
|
||
## 参数契约
|
||
|
||
- `districtId`:Long,必填,正整数。
|
||
- `startDate` / `endDate`:String,必填,yyyy-MM-dd,含当天;startDate ≤ endDate;endDate 不得晚于今天(决策 5);
|
||
区间自然日数 ≤ Redis `Dictionary.StatisticsDays`(缺失/非法 fail fast)。
|
||
- `view`:String,必填,`gender`(占比)或 `daily_gender`(按日趋势)。
|
||
- `deviceType`:Integer,可选,非负整数透传 `device_type =`(决策 4)。
|
||
- `buildingIds` / `floors` / `toiletIds`:List,可选,元素须正整数/非空串;各维度内部 IN、维度之间 AND
|
||
(对齐 34 号工具;替代老 idStrList 拼串,决策 3)。
|
||
|
||
固定条件:`soft_delete=0`、`district_id=`、`statistics_time` 在区间内。
|
||
聚合在 DB 完成(GROUP BY gender / DATE_FORMAT(statistics_time)+gender),不查 ES。
|
||
|
||
## 返回契约
|
||
|
||
公共:`districtId / districtName / dateRange{startDate,endDate,days} / view / unit="人次"`。
|
||
|
||
`view=gender`(对齐老饼图 PieStatisticsOutput):
|
||
|
||
```
|
||
totalCount 区间内 SUM(use_count)
|
||
rows[]: genderCode / gender / useCount / percent percent=useCount*100/totalCount,HALF_UP 两位;totalCount=0 时 percent=0
|
||
notice? 无数据时说明
|
||
```
|
||
|
||
`view=daily_gender`(对齐老折线 LineChartStatisticsOutput,修正零填充错位 bug):
|
||
|
||
```
|
||
dates[] 区间内全部日期(yyyy-MM-dd 升序)
|
||
totalCount
|
||
series[]: genderCode / gender / useCount(合计) / values[] values 与 dates 等长对齐,无数据日期为 0
|
||
notice?
|
||
```
|
||
|
||
gender 文案:0男卫、1女卫、2母婴室、3无障碍、4第三卫、5其他;null 为「未配置」,其他整数 `未知(code)`
|
||
(与 34 号工具一致)。
|
||
|
||
## 与老系统的差异汇总
|
||
|
||
1. 两个接口合并为一个工具的 view 参数。
|
||
2. 行级权限:老系统注释掉了,新工具按同表工具惯例做 `applyDistrictRowFilter`(待拍板,决策 2)。
|
||
3. idStrList 拼串改为 buildingIds/floors/toiletIds 三列表(决策 3)。
|
||
4. 折线零填充错位 bug 修正(老 :518-525)。
|
||
5. gender=null 归「未配置」,不再 NPE。
|
||
6. Redis StatisticsDays 缺失 fail fast(老系统 try/catch 吞掉)。
|
||
7. endDate 不得晚于今天(老系统不限制,决策 5)。
|
||
|
||
## 测试要点
|
||
|
||
- 参数校验:缺 districtId/日期、startDate>endDate、endDate 晚于今天、超 StatisticsDays、view 非法、
|
||
deviceType 负数、位置列表非法元素。
|
||
- 权限:站点不可见返回「站点不存在或无权访问」(若决策 2 通过);tenant_service 按总部视角。
|
||
- view=gender:多 gender 聚合、percent 两位小数、totalCount=0 时 percent=0、未知 code、null gender 归未配置。
|
||
- view=daily_gender:零填充序列与 dates 等长对齐(回归老系统插入错位 bug)、跨月区间、空数据。
|
||
- 位置三维 IN/AND 语义、deviceType 透传、StatisticsDays 缺失 fail fast。
|