修正用纸统计 useCount 口径:仅厕位模式(runl_mode=0)厕位行累计,对齐老代码意图并消除其分支不一致 bug

This commit is contained in:
my_local_kimi_dev2
2026-09-20 18:53:51 +08:00
parent 8d8e71e45c
commit 47d5e8d545
4 changed files with 37 additions and 7 deletions
@@ -755,7 +755,7 @@
- 入参:`districtId`(必填正整数)、`startDate`/`endDate`(必填 yyyy-MM-dd,startDate≤endDate≤今天)、 - 入参:`districtId`(必填正整数)、`startDate`/`endDate`(必填 yyyy-MM-dd,startDate≤endDate≤今天)、
`positionMode`(position=仅厕位/wall=仅厕墙,只作用于 ES 换纸卷数口径)、`deviceType`(0/1)、`buildingId`、`floor`、`toiletId`。 `positionMode`(position=仅厕位/wall=仅厕墙,只作用于 ES 换纸卷数口径)、`deviceType`(0/1)、`buildingId`、`floor`、`toiletId`。
- 聚合维度 gender(0男1女2母婴3残卫4第三5其他99未知): - 聚合维度 gender(0男1女2母婴3残卫4第三5其他99未知):
`paperLengthM`=Σpaper_length/100(米,3 位小数)、`useCount`=Σuse_count(厕墙行恒 0 直接求和)、 `paperLengthM`=Σpaper_length/100(米,3 位小数)、`useCount`=Σuse_count(仅厕位模式 runl_mode=0 的厕位行累计,厕墙行恒 0)、
`deviceDayCount`=按日 distinct 厕位/厕数之和(老系统 perPositionsCount,输出改名)、 `deviceDayCount`=按日 distinct 厕位/厕数之和(老系统 perPositionsCount,输出改名)、
`perUseLengthM`=paperLengthM/useCount(useCount=0 为 null);另输出按日明细(date/gender/paperLengthM/useCount/deviceDayCount)。 `perUseLengthM`=paperLengthM/useCount(useCount=0 为 null);另输出按日明细(date/gender/paperLengthM/useCount/deviceDayCount)。
- 换纸卷数 `rfidCount`(ES):读 `district_strategy.rfid_use_flag`(行 id=站点 id)—— - 换纸卷数 `rfidCount`(ES):读 `district_strategy.rfid_use_flag`(行 id=站点 id)——
@@ -19,8 +19,8 @@
(实现为左闭右开 `[start, end+1)` 避免秒精度丢行);可选 `building_id/floor/toilet_id/device_type` 精确匹配。 (实现为左闭右开 `[start, end+1)` 避免秒精度丢行);可选 `building_id/floor/toilet_id/device_type` 精确匹配。
- 聚合维度 `gender`(gender 为 null 的行跳过,老 :178-180): - 聚合维度 `gender`(gender 为 null 的行跳过,老 :178-180):
- `paperLengthM` = Σpaper_length / 100(paper_length 单位厘米,输出米,3 位小数); - `paperLengthM` = Σpaper_length / 100(paper_length 单位厘米,输出米,3 位小数);
- `useCount` = Σuse_count(老代码对厕位/厕墙、runl_mode 有多个分支,但厕墙行 use_count 恒 0, - `useCount` = Σuse_count(**仅 position_id>0 且 runl_mode=0 的厕位模式行累计**,厕墙行恒 0;
等价于直接求和——老 :188-203 分支差异不影响结果); 这是老 :200-203、:220-222 分支的意图——老 :188 的日分组已存在分支漏判 runlMode,按意图统一);
- `deviceDayCount` = 按日 distinct 厕位/厕数之和(老 :190-192、:249:positionSet 存 - `deviceDayCount` = 按日 distinct 厕位/厕数之和(老 :190-192、:249:positionSet 存
`p_{positionId}`(厕位行)与 `t_{toiletId}`(厕墙行),每天取 set size 再累加)。 `p_{positionId}`(厕位行)与 `t_{toiletId}`(厕墙行),每天取 set size 再累加)。
老出参字段名 perPositionsCount 注释有误,本工具改名 deviceDayCount); 老出参字段名 perPositionsCount 注释有误,本工具改名 deviceDayCount);
@@ -91,7 +91,7 @@
| rfid 去重实现 | count+分页拉全量文档,内存 Set 去重 | terms 聚合 size 10000 数 bucket | 不拉全量、低内存低耗时;上限 10000 对正常站点足够,超限部分不计(见 §2.2) | | rfid 去重实现 | count+分页拉全量文档,内存 Set 去重 | terms 聚合 size 10000 数 bucket | 不拉全量、低内存低耗时;上限 10000 对正常站点足够,超限部分不计(见 §2.2) |
| district_strategy 不存在 / rfid_use_flag=null | NPE(:324-325) | 按"未使用 rfid"口径走 state=2 count | 该表与站点 1:1,缺失视为未启用 rfid;避免 500 | | district_strategy 不存在 / rfid_use_flag=null | NPE(:324-325) | 按"未使用 rfid"口径走 state=2 count | 该表与站点 1:1,缺失视为未启用 rfid;避免 500 |
| Dictionary 字典缺失 | NPE 被外层 catch 吞掉,继续执行 | 抛 IllegalStateException | fail fast,避免权限/口径配置异常导致错误数据范围 | | Dictionary 字典缺失 | NPE 被外层 catch 吞掉,继续执行 | 抛 IllegalStateException | fail fast,避免权限/口径配置异常导致错误数据范围 |
| use_count 聚合 | 厕位/厕墙/runl_mode 多分支 | 直接求和 | 厕墙行恒 0,分支差异不影响结果,简化口径 | | use_count 聚合 | 厕位/厕墙/runl_mode 多分支,:188 已存在日分组分支漏判 runlMode | 仅 position_id>0 且 runl_mode=0 行累计 | 统一为老代码新分组分支的意图口径(厕位模式使用人次),消除老代码分支不一致的 bug |
| DB 时间过滤 | between 含两端(秒精度) | 左闭右开 [start, end+1) | 等价且避免 23:59:59 之后毫秒级数据丢行 | | DB 时间过滤 | between 含两端(秒精度) | 左闭右开 [start, end+1) | 等价且避免 23:59:59 之后毫秒级数据丢行 |
## 6. 行级权限说明 ## 6. 行级权限说明
@@ -233,13 +233,21 @@ public class DistrictPaperStatisticsTools implements ToolObject {
int paperLength = row.getPaperLength() == null ? 0 : row.getPaperLength(); int paperLength = row.getPaperLength() == null ? 0 : row.getPaperLength();
int useCount = row.getUseCount() == null ? 0 : row.getUseCount(); int useCount = row.getUseCount() == null ? 0 : row.getUseCount();
agg.paperCm += paperLength; agg.paperCm += paperLength;
agg.useCount += useCount; boolean positionRow = row.getPositionId() != null && row.getPositionId() > 0;
// useCount 仅厕位模式(runl_mode=0)的厕位行累计,厕墙恒 0(老 :200-203/220-222 意图;
// 老代码日分组已存在分支漏判 runlMode,此处按意图统一,见设计文档差异点)
boolean countUse = positionRow && row.getRunlMode() != null && row.getRunlMode() == 0;
if (countUse) {
agg.useCount += useCount;
}
String day = DAY_FORMAT.format(row.getStatisticsTime().toInstant().atZone(SHANGHAI)); String day = DAY_FORMAT.format(row.getStatisticsTime().toInstant().atZone(SHANGHAI));
DailyAgg dailyAgg = agg.daily.computeIfAbsent(day, key -> new DailyAgg()); DailyAgg dailyAgg = agg.daily.computeIfAbsent(day, key -> new DailyAgg());
dailyAgg.paperCm += paperLength; dailyAgg.paperCm += paperLength;
dailyAgg.useCount += useCount; if (countUse) {
dailyAgg.useCount += useCount;
}
// 厕位行按 positionId、厕墙行按 toiletId 计台(老 "p_"/"t_" positionSet) // 厕位行按 positionId、厕墙行按 toiletId 计台(老 "p_"/"t_" positionSet)
if (row.getPositionId() != null && row.getPositionId() > 0) { if (positionRow) {
dailyAgg.positionKeys.add("p_" + row.getPositionId()); dailyAgg.positionKeys.add("p_" + row.getPositionId());
} else { } else {
dailyAgg.positionKeys.add("t_" + row.getToiletId()); dailyAgg.positionKeys.add("t_" + row.getToiletId());
@@ -116,6 +116,7 @@ class DistrictPaperStatisticsToolsTest {
row.setToiletId(toiletId); row.setToiletId(toiletId);
row.setPaperLength(paperLengthCm); row.setPaperLength(paperLengthCm);
row.setUseCount(useCount); row.setUseCount(useCount);
row.setRunlMode(0);
return row; return row;
} }
@@ -285,6 +286,27 @@ class DistrictPaperStatisticsToolsTest {
anyLong(), anyLong(), any(), any()); anyLong(), anyLong(), any(), any());
} }
@Test
void useCountOnlyAccumulatesForPositionModeRows() {
mockDistrictAndDictionary();
MonthlyStatistics wallModeRow = row(today.minusDays(1), 11L, 3L, 0, 100, 5);
wallModeRow.setRunlMode(1);
when(monthlyStatisticsMapper.selectList(any())).thenReturn(List.of(
wallModeRow,
row(today.minusDays(1), 11L, 3L, 0, 100, 2)));
when(districtStrategyMapper.selectOne(any())).thenReturn(null);
when(esGateway.countState2(anyString(), anyBoolean(), anyLong(), anyInt(), anyLong(), anyLong(),
any(), any())).thenReturn(0L);
Map<String, Object> result = tools.districtPaperStatisticsQuery(9L, start, end, null, null, null, null, null);
@SuppressWarnings("unchecked")
List<Map<String, Object>> summary = (List<Map<String, Object>>) result.get("summaryByGender");
// runl_mode=1 的厕位行 useCount 不计入,仅厕位模式(runl_mode=0)累计
assertEquals(2L, summary.get(0).get("useCount"));
assertEquals(2.0, summary.get(0).get("paperLengthM"));
}
@Test @Test
void useCountZeroYieldsNullPerUseLength() { void useCountZeroYieldsNullPerUseLength() {
mockDistrictAndDictionary(); mockDistrictAndDictionary();