فهرست منبع

预警清单添加统计数据

zengy 3 هفته پیش
والد
کامیت
bc0b541977

+ 9 - 5
docs/alarm-operations-2026-09-24.md

@@ -2,21 +2,25 @@
 
 ## 交付入口
 
-后端统一在 `AlarmController`,查询服务/DTO/枚举/Mapper 位于 `zksy-system/base/alarm`。前端公共报警页面及组件位于 `src/views/subSystem/alarm`,排水研判和 GIS 位于 `src/views/subSystem/drainage/alarm`,供水 GIS 位于 `src/views/subSystem/waterSupply/alarm`。
+后端统一在 `AlarmController`,查询服务/DTO/枚举/Mapper 位于 `zksy-system/base/alarm`。前端公共报警页面及配套 utils/styles 位于 `src/views/subSystem/alarm/common`,详情等共享组件仍位于 `src/views/subSystem/alarm`;排水研判和 GIS 位于 `src/views/subSystem/drainage/alarm`,供水 GIS 位于 `src/views/subSystem/waterSupply/alarm`。
+
+已有环境本次执行 `pipe-network-service/sql/migration_alarm_common_components.sql`,精确更新 2118/2142/2097/2024/2117/2140/2139 的 component,其余菜单属性保持不变;同步部署前后端后重新登录。初装审核及运营脚本已使用 common 路径。
 
 | 既有菜单 | path | component | query |
 | --- | --- | --- | --- |
-| 2139 | alarmRemind | subSystem/alarm/remind | {"rootTypeId":2} |
-| 2140 | alarmList | subSystem/alarm/list | {"rootTypeId":2} |
+| 2139 | alarmRemind | subSystem/alarm/common/remind | {"rootTypeId":2} |
+| 2140 | alarmList | subSystem/alarm/common/list | {"rootTypeId":2} |
 | 2141 | drainageAlarmAnalysis | subSystem/drainage/alarm/analysis | {"rootTypeId":2} |
 | 2143 | drainageAlarmGIS | subSystem/drainage/alarm/alarmGIS | {"rootTypeId":2} |
-| 2117 | alarmList | subSystem/alarm/list | {"rootTypeId":1} |
+| 2117 | alarmList | subSystem/alarm/common/list | {"rootTypeId":1} |
 | 2120 | waterSupplyAlarmGIS | subSystem/waterSupply/alarm/alarmGIS | {"rootTypeId":1} |
 
 GIS 按最终要求命名为 `alarmGIS.vue`,没有“周边环境”页签;保留设备信息、报警记录、监测曲线。菜单更新不创建新页面菜单,不调整父级、排序和角色关联。
 
 ## 查询与交互
 
+- 报警清单顶部新增报警总数、监测异常、监测已恢复三项统计。复用 `GET /api/alarm/statistics` 的分级结果,在 level/total 之外增加 abnormal(未处理)、recovered(已处理);状态通过 `AlarmRecordStatusEnum` 传入 SQL,原提醒的分级 total 不变。三项数据均覆盖当前条件和授权范围的全部事件,不受分页影响;未知状态只计入总数。需同步部署前端和管理后端,无数据库结构或菜单变更(2026-09-24)。
+
 - `GET /api/alarm`:框架分页,后端时间排序;支持报警单号、设备关键字、报警等级、监测状态、审核结果、日期。返回 `data.records/total`,清单不再只看 `alarm_status=0`。报警单号为清单和审核列表第一列。清单页面日期条件标题为“报警时间”,设备搜索提示“请输入设备编码/名称”,移除审核结果查询条件但保留表格的审核结果列;通用接口兼容已有审核结果参数。
 - `GET /api/alarm/statistics`、`/reminders`、`/options`:等级统计、按最近操作排序的事件提醒、枚举选项。提醒服务固定 `alarm_status=0`,不接受请求参数切换到已处理事件;页面移除“报警清单”操作,保留事件详情。每 30 秒更新,手动、悬停、聚焦、打开详情时暂停滚动,继续缓冲更新。
 - `GET /api/alarm/{alarmId}`、`/{alarmId}/timeline`:详情与报警操作历史,时间倒序、同一时间按日志 ID 倒序;人工操作显示用户昵称,前端按日期分组,附件图片缩略图可预览。工单操作不再复制到报警日志,确认日志直接显示“已转工单(实际工单号)”。
@@ -25,7 +29,7 @@ GIS 按最终要求命名为 `alarmGIS.vue`,没有“周边环境”页签;
 
 GIS 监测曲线选项/曲线操作与研判事件下拉只展示 `alarm_data.actual_value` 非空的事件,数值 0 正常保留。GIS 点位和报警记录仍包含无实际值的报警,不按此条件缩减地图。研判下拉请求 `GET /api/alarm?actualValueOnly=true`,数据库在分页之前加 `actual_value IS NOT NULL`,总数同步遵循此条件;默认 false 不影响普通清单。详情进入研判的按钮同样校验实际值,深链接不将无实际值事件补入候选,曲线接口对无实际值返回 unsupported/空采样且不查询采集表。
 
-所有新接口要求 `alarm:operations:list`,同时服务端按用户获授权的菜单校验 `rootTypeId` 和可选 `warningCode`,并查询设备类型根及全部后代。空设备类型范围返回空,不退化为无限制查询。审核授权仍单独校验 `subSystem/alarm/audit`,有清单权限不等于有审核权限。地图和曲线接口接受供水及排水根类型,独立研判页面仍只用于排水。
+所有新接口要求 `alarm:operations:list`,同时服务端按用户获授权的菜单校验 `rootTypeId` 和可选 `warningCode`,并查询设备类型根及全部后代。空设备类型范围返回空,不退化为无限制查询。审核授权仍单独校验 `subSystem/alarm/common/audit`,有清单权限不等于有审核权限。地图和曲线接口接受供水及排水根类型,独立研判页面仍只用于排水。
 
 2117 同时保留 `waterSupply:alarm:list`(框架支持逗号分隔的权限),供供水首页的统计和实时异常接口使用。供水地图及首页报警点位已使用统一地图接口。
 

+ 3 - 3
docs/alarm-review-2026-09-23.md

@@ -2,7 +2,7 @@
 
 ## 已确认契约
 
-- 前端 `city-life-line/src/views/subSystem/alarm/audit.vue`,后端 `AlarmController`,独立的审核服务位于 `base/alarm`。
+- 前端 `city-life-line/src/views/subSystem/alarm/common/audit.vue`,后端 `AlarmController`,独立的审核服务位于 `base/alarm`。
 - 仅 `alarm_status=0` 进入审核列表,一条事件一行,不按设备去重;后端分页和时间排序。
 - `audit_status`:0 未审核、1 确认报警、2 误报警。`alarm_level` 继续使用 1~4(一级最严重,红/橙/蓝/绿),不新增或更名为 `case_level`。
 - 确认报警原子更新审核状态、记录审核日志并创建工单;任一步失败整个审核事务回滚。不修改设备状态,也不将报警标为已处理。误报警仅记录结论及意见。
@@ -20,8 +20,8 @@
 - 管理端与采集端在新增/处理事务中主动插入 `alarm_log`,不依赖日志触发器已部署。新增记录“系统触发报警;报警描述:…”,描述优先取非空备注,否则取报警类型/编码。采集自动恢复记录“监测恢复,报警自动解除”,仅状态真正变为 1 时记录,重复正常上报不重复记日志。部署脚本删除旧生命周期及工单日志镜像触发器/自定义函数,不再由数据库自动写日志。人工处理仍记报警日志;工单关联、工单操作不再单独写入 `alarm_log`,确认日志直接包含“已转工单(实际工单号)”及选填的审核意见。日志写入失败与业务变更一起回滚。最新单号规则为程序生成 `BJyyyyMMddHHmmss + 4位随机数`,见运营交付说明。
 - 人工操作写入 `sys_user.nick_name` 与用户 ID;历史查询按用户 ID 关联昵称,用户不存在时保留原日志名称。按 `oper_time DESC NULLS LAST, log_id DESC` 返回,审核页和详情页共用日期分组历史组件,样式参考工单 `dispose.vue`,图片保留缩略图和大图预览。
 - 日志枚举在 `zksy-system` 与 `zk-api-service` 各定义一份 `AlarmLogOperTypeEnum`,两端值保持一致:0 报警触发、1 确认报警、2 误报警、3 报警处理。不新增共享模块,移除关联工单/工单操作类型,历史旧类型日志保留并显示“历史操作”。已有环境执行增量脚本 `migration_alarm_review_log.sql`,不迁移或删除历史数据。
-- 精准更新已有菜单 2118/2142/2097/2024,`path='alarmAudit'`、`component='subSystem/alarm/audit'`。设备类型根分别为 1/2/3/4,燃气额外 `warning_code='WARN-GAS-CONCENTRATION'`;保留原父级和角色关联。
-- 已部署环境仅因页面改名需要更新菜单时,执行 `pipe-network-service/sql/migration_alarm_menu_components.sql`,同步更新前后端后重新登录,无需重复执行建表或权限初始化。
+- 精准更新已有菜单 2118/2142/2097/2024,`path='alarmAudit'`、`component='subSystem/alarm/common/audit'`。设备类型根分别为 1/2/3/4,燃气额外 `warning_code='WARN-GAS-CONCENTRATION'`;保留原父级和角色关联。
+- 已部署环境本次公共页面移入 `alarm/common`,执行 `pipe-network-service/sql/migration_alarm_common_components.sql` 精准更新七个审核/清单/提醒菜单的 component;同步更新前后端后重新登录,无需重复执行建表或权限初始化,保留 path/query/权限和角色关联。
 - 页面权限为 `alarm:review:list`;仅在缺失时创建 `alarm:review:audit` 按钮权限,给对应页面的现有角色补充新按钮授权,重复执行不恢复已经撤销的按钮授权。不会重复新增四个页面菜单。
 - `rootTypeId` 代表 `equipment_type.id` 的根,查询包含所有后代;服务端校验当前用户实际获授权的菜单范围,不能仅修改前端 query 跨系统访问。
 

+ 2 - 2
pipe-network-service/CONTEXT.md

@@ -51,9 +51,9 @@ _Avoid_:把供水子系统等同于单一的报警页面,或把监测设备
 
 ### 4. 监测报警
 
-4.1 **报警管理**:供水报警管理与排水报警清单统一到 `subSystem/alarm/list`,展示报警单号、设备信息、监测/审核/工单状态与责任人,按菜单 `rootTypeId` 校验授权并过滤设备类型树。清单只承载查询与详情,审核、派发和处置仍在相应页面。排水另有 `remind` 提醒、`drainage/analysis` 研判、`drainage/alarmGIS` GIS(无周边环境页签)。新清单替代旧管理页的态势/占比/导出分支;供水原有报警一张图继续保留。不同报警类型可形成不同工单;同一设备同一报警类型同时只能存在一个未结束的同类报警工单(2026-09-24)。
+4.1 **报警管理**:供水报警管理与排水报警清单统一到 `subSystem/alarm/common/list`,展示报警单号、设备信息、监测/审核/工单状态与责任人,按菜单 `rootTypeId` 校验授权并过滤设备类型树。清单只承载查询与详情,审核、派发和处置仍在相应页面。排水另有 `remind` 提醒、`drainage/analysis` 研判、`drainage/alarmGIS` GIS(无周边环境页签)。新清单替代旧管理页的态势/占比/导出分支;供水原有报警一张图继续保留。不同报警类型可形成不同工单;同一设备同一报警类型同时只能存在一个未结束的同类报警工单(2026-09-24)。
 
-4.2 **报警审核**:四个子系统统一使用 `AlarmController` 的审核接口和 `subSystem/alarm/audit` 页面,列表仅展示 `alarm_status=0` 的事件。`audit_status` 独立记录未审核、确认报警、误报警;确认报警时创建待派单工单,误报警只记录结论,两种审核都不直接修改设备状态或报警处理状态。完整派单、接单、处理、验收由统一工单页承载。旧供水审核、直接派单和停用的解除接口已删除。报警生成、审核及处理记录在 `alarm_log`,确认记录直接包含“已转工单(实际工单号)”及选填意见;关联工单、工单操作不再单独写入报警日志。人工操作使用 `sys_user.nick_name`,历史按时间和日志 ID 倒序展示,不回填或删除历史数据。运行采集系统在收到有效恢复数据时,可以自动处理同设备、同报警编码的未处理告警;数值回到预警区即可处理,处理记录注明“系统自动”,不自动完成关联工单(见仓库docs/collection-alarm-recovery-2026-09-23.md)。
+4.2 **报警审核**:四个子系统统一使用 `AlarmController` 的审核接口和 `subSystem/alarm/common/audit` 页面,列表仅展示 `alarm_status=0` 的事件。`audit_status` 独立记录未审核、确认报警、误报警;确认报警时创建待派单工单,误报警只记录结论,两种审核都不直接修改设备状态或报警处理状态。完整派单、接单、处理、验收由统一工单页承载。旧供水审核、直接派单和停用的解除接口已删除。报警生成、审核及处理记录在 `alarm_log`,确认记录直接包含“已转工单(实际工单号)”及选填意见;关联工单、工单操作不再单独写入报警日志。人工操作使用 `sys_user.nick_name`,历史按时间和日志 ID 倒序展示,不回填或删除历史数据。运行采集系统在收到有效恢复数据时,可以自动处理同设备、同报警编码的未处理告警;数值回到预警区即可处理,处理记录注明“系统自动”,不自动完成关联工单(见仓库docs/collection-alarm-recovery-2026-09-23.md)。
 
 4.3 **阈值管理**:支持按传感器分类批量设置阈值、设置分时阈值并查询已配置点位的阈值详情。
 

+ 15 - 0
pipe-network-service/sql/migration_alarm_common_components.sql

@@ -0,0 +1,15 @@
+-- 报警公共页面移动到 subSystem/alarm/common 后,更新已有菜单组件路径。
+-- KingbaseES;可重复执行。与新版前后端同步部署,执行后重新登录以刷新动态菜单。
+-- 保留现有 path、query、权限、父级及角色菜单关联。
+BEGIN;
+
+UPDATE app_user.sys_menu
+SET component = CASE
+        WHEN menu_id IN (2118, 2142, 2097, 2024) THEN 'subSystem/alarm/common/audit'
+        WHEN menu_id IN (2117, 2140) THEN 'subSystem/alarm/common/list'
+        WHEN menu_id = 2139 THEN 'subSystem/alarm/common/remind'
+    END,
+    update_time = CURRENT_TIMESTAMP
+WHERE menu_id IN (2118, 2142, 2097, 2024, 2117, 2140, 2139);
+
+COMMIT;

+ 2 - 2
pipe-network-service/sql/migration_alarm_operations.sql

@@ -25,9 +25,9 @@ DROP FUNCTION IF EXISTS app_user.record_alarm_work_order_log();
 UPDATE app_user.sys_menu SET
     path = CASE menu_id WHEN 2139 THEN 'alarmRemind' WHEN 2140 THEN 'alarmList'
         WHEN 2141 THEN 'drainageAlarmAnalysis' WHEN 2143 THEN 'drainageAlarmGIS' WHEN 2117 THEN 'alarmList' END,
-    component = CASE menu_id WHEN 2139 THEN 'subSystem/alarm/remind' WHEN 2140 THEN 'subSystem/alarm/list'
+    component = CASE menu_id WHEN 2139 THEN 'subSystem/alarm/common/remind' WHEN 2140 THEN 'subSystem/alarm/common/list'
         WHEN 2141 THEN 'subSystem/drainage/alarm/analysis' WHEN 2143 THEN 'subSystem/drainage/alarm/alarmGIS'
-        WHEN 2117 THEN 'subSystem/alarm/list' END,
+        WHEN 2117 THEN 'subSystem/alarm/common/list' END,
     query = CASE WHEN menu_id = 2117 THEN '{"rootTypeId":1}' ELSE '{"rootTypeId":2}' END,
     -- 框架按逗号拆分权限;供水首页的统计和实时异常仍使用原查询接口。
     perms = CASE WHEN menu_id = 2117 THEN 'alarm:operations:list,waterSupply:alarm:list'

+ 1 - 1
pipe-network-service/sql/migration_alarm_review.sql

@@ -54,7 +54,7 @@ DROP FUNCTION IF EXISTS app_user.record_alarm_lifecycle();
 DROP FUNCTION IF EXISTS app_user.record_alarm_work_order_log();
 
 -- 四条都是已有页面菜单:精准更新 ID,统一 path,保留父级、排序、图标与已有角色关联。
-UPDATE app_user.sys_menu SET path = 'alarmAudit', component = 'subSystem/alarm/audit', perms = 'alarm:review:list',
+UPDATE app_user.sys_menu SET path = 'alarmAudit', component = 'subSystem/alarm/common/audit', perms = 'alarm:review:list',
     query = CASE menu_id
         WHEN 2118 THEN '{"rootTypeId":1}'
         WHEN 2142 THEN '{"rootTypeId":2}'

+ 1 - 1
pipe-network-service/sql/tests/alarm_review.test.mjs

@@ -40,7 +40,7 @@ test('four existing menu IDs get alarmAudit and exact independent scopes, unrela
   const menus = await rows('SELECT * FROM app_user.sys_menu WHERE menu_id IN (2118,2142,2097,2024) ORDER BY menu_id')
   const scope = { 2118:{rootTypeId:1}, 2142:{rootTypeId:2}, 2097:{rootTypeId:3,warning_code:'WARN-GAS-CONCENTRATION'}, 2024:{rootTypeId:4} }
   for (const menu of menus) {
-    assert.equal(menu.path, 'alarmAudit'); assert.equal(menu.component, 'subSystem/alarm/audit')
+    assert.equal(menu.path, 'alarmAudit'); assert.equal(menu.component, 'subSystem/alarm/common/audit')
     assert.deepEqual(JSON.parse(menu.query), scope[menu.menu_id]); assert.equal(menu.perms,'alarm:review:list')
   }
   assert.equal((await rows('SELECT path FROM app_user.sys_menu WHERE menu_id=3000'))[0].path,'keep')

+ 2 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/mapper/AlarmOperationsMapper.java

@@ -14,7 +14,8 @@ public interface AlarmOperationsMapper {
     Page<AlarmVO> selectPage(Page<AlarmVO> page, @Param("query") AlarmQuery query, @Param("typeIds") List<Integer> typeIds);
     Page<AlarmVO> selectReminders(Page<AlarmVO> page, @Param("query") AlarmQuery query, @Param("typeIds") List<Integer> typeIds);
     AlarmVO selectDetail(@Param("id") String id, @Param("query") AlarmQuery query, @Param("typeIds") List<Integer> typeIds);
-    List<Map<String, Object>> countByLevel(@Param("query") AlarmQuery query, @Param("typeIds") List<Integer> typeIds);
+    List<Map<String, Object>> countByLevel(@Param("query") AlarmQuery query, @Param("typeIds") List<Integer> typeIds,
+            @Param("unhandledStatus") int unhandledStatus, @Param("handledStatus") int handledStatus);
     AlarmDashboardVO selectDashboardCounts(@Param("query") AlarmQuery query, @Param("typeIds") List<Integer> typeIds,
             @Param("deviceAlarmStatus") int deviceAlarmStatus);
     List<Map<String, Object>> selectDashboardTrend(@Param("query") AlarmQuery query, @Param("typeIds") List<Integer> typeIds,

+ 2 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmOperationsService.java

@@ -54,7 +54,8 @@ public class AlarmOperationsService {
     }
     public List<Map<String, Object>> statistics(AlarmQuery query) {
         validate(query);
-        return mapper.countByLevel(query, scope.resolveOperations(query));
+        return mapper.countByLevel(query, scope.resolveOperations(query),
+                AlarmRecordStatusEnum.UNHANDLED.getCode(), AlarmRecordStatusEnum.HANDLED.getCode());
     }
     /** 首页统计保留全量口径,仅趋势限制日期;所有聚合在数据库内完成。 */
     public AlarmDashboardVO dashboard(AlarmQuery query, int days) {

+ 3 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewScopeService.java

@@ -23,12 +23,12 @@ public class AlarmReviewScopeService {
     private final ISysMenuService menuService;
 
     public List<Integer> resolve(AlarmReviewQuery query) {
-        return resolve(query, Collections.singleton("subSystem/alarm/audit"));
+        return resolve(query, Collections.singleton("subSystem/alarm/common/audit"));
     }
 
     /** 读取与审核授权独立,列表菜单不能授予跨系统审核能力。 */
     public List<Integer> resolveOperations(AlarmReviewQuery query) {
-        return resolve(query, new HashSet<>(Arrays.asList("subSystem/alarm/remind", "subSystem/alarm/list",
+        return resolve(query, new HashSet<>(Arrays.asList("subSystem/alarm/common/remind", "subSystem/alarm/common/list",
                 "subSystem/drainage/alarm/analysis", "subSystem/drainage/alarm/alarmGIS",
                 "subSystem/waterSupply/alarm/alarmGIS")));
     }
@@ -56,7 +56,7 @@ public class AlarmReviewScopeService {
     }
 
     boolean matches(SysMenu menu, AlarmReviewQuery query) {
-        return matches(menu, query, Collections.singleton("subSystem/alarm/audit"));
+        return matches(menu, query, Collections.singleton("subSystem/alarm/common/audit"));
     }
 
     private boolean matches(SysMenu menu, AlarmReviewQuery query, Set<String> components) {

+ 4 - 1
pipe-network-service/zksy-system/src/main/resources/mapper/base/AlarmOperationsMapper.xml

@@ -52,7 +52,10 @@
         ORDER BY latest_operation_time DESC NULLS LAST, a.id DESC
     </select>
     <select id="countByLevel" resultType="java.util.Map">
-        SELECT a.alarm_level AS level, COUNT(*) AS total <include refid="joins"/>
+        SELECT a.alarm_level AS level, COUNT(*) AS total,
+            COUNT(CASE WHEN a.alarm_status = #{unhandledStatus} THEN 1 END) AS abnormal,
+            COUNT(CASE WHEN a.alarm_status = #{handledStatus} THEN 1 END) AS recovered
+        <include refid="joins"/>
         <where><include refid="filters"/></where> GROUP BY a.alarm_level
     </select>
     <select id="selectMap" resultMap="alarm">

+ 14 - 1
pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/mapper/AlarmOperationsMapperTest.java

@@ -21,15 +21,28 @@ class AlarmOperationsMapperTest {
         }
         AlarmQuery query = new AlarmQuery(); query.setRootTypeId(2);
         Map<String,Object> params = new HashMap<>(); params.put("query",query); params.put("typeIds",Arrays.asList(2,21));
+        params.put("unhandledStatus",com.zksy.base.alarm.enums.AlarmRecordStatusEnum.UNHANDLED.getCode());
+        params.put("handledStatus",com.zksy.base.alarm.enums.AlarmRecordStatusEnum.HANDLED.getCode());
         Map<String,String> rendered = new LinkedHashMap<>();
         for (String name : Arrays.asList("selectPage","selectDetail","selectReminders","countByLevel","selectMap")) {
             query.setAlarmStatus("selectReminders".equals(name) ? 0 : null);
             String sql = sql(config,name,params);
             assertTrue(sql.contains("e.equipment_type_id IN"));
-            assertEquals("selectReminders".equals(name),sql.contains("a.alarm_status ="));
+            assertEquals("selectReminders".equals(name),sql.contains("AND a.alarm_status ="));
             assertFalse(sql.contains("a.actual_value IS NOT NULL"));
             assertTrue(sql.contains("w.order_id = a.work_order_id")); rendered.put(name,sql);
         }
+        String statisticsSql = rendered.get("countByLevel");
+        assertTrue(statisticsSql.contains("COUNT(*) AS total"));
+        assertTrue(statisticsSql.contains("COUNT(CASE WHEN a.alarm_status = ? THEN 1 END) AS abnormal"));
+        assertTrue(statisticsSql.contains("COUNT(CASE WHEN a.alarm_status = ? THEN 1 END) AS recovered"));
+        assertFalse(statisticsSql.contains("OFFSET"));
+        assertTrue(statisticsSql.endsWith("GROUP BY a.alarm_level"));
+        query.setAlarmStatus(1);
+        assertTrue(sql(config,"countByLevel",params).contains("AND a.alarm_status = ?"));
+        params.put("typeIds",Collections.emptyList());
+        assertTrue(sql(config,"countByLevel",params).contains("1 = 0"));
+        params.put("typeIds",Arrays.asList(2,21)); query.setAlarmStatus(null);
         query.setKeyword("设备搜索");
         for (String name : Arrays.asList("selectPage", "selectReminders", "countByLevel", "selectMap")) {
             String keywordSql = sql(config,name,params);

+ 15 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmOperationsServiceTest.java

@@ -41,6 +41,21 @@ class AlarmOperationsServiceTest {
         assertEquals("确认报警",alarm.getAuditStatusName()); assertEquals("处理中",alarm.getOrderStatusName());
         verify(scope).resolveOperations(query);
     }
+    @Test void statisticsKeepsFiltersAndResolvesAuthorizedScopeBeforeAggregating() {
+        query.setPageNum(3); query.setPageSize(10); query.setKeyword("设备");
+        query.setAlarmStatus(AlarmRecordStatusEnum.HANDLED.getCode());
+        List<Map<String,Object>> counts = Collections.singletonList(
+                Map.of("level",2,"total",30L,"abnormal",0L,"recovered",30L));
+        when(mapper.countByLevel(query,Arrays.asList(2,21),
+                AlarmRecordStatusEnum.UNHANDLED.getCode(),AlarmRecordStatusEnum.HANDLED.getCode())).thenReturn(counts);
+        assertSame(counts,service.statistics(query));
+        assertEquals(AlarmRecordStatusEnum.HANDLED.getCode(),query.getAlarmStatus());
+        verify(scope).resolveOperations(query);
+        reset(mapper);
+        when(scope.resolveOperations(query)).thenThrow(new org.springframework.security.access.AccessDeniedException("denied"));
+        assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> service.statistics(query));
+        verifyNoInteractions(mapper);
+    }
     @Test void outOfScopeDetailAndTimelineCannotExposeLogs() {
         when(mapper.selectDetail(anyString(),eq(query),anyList())).thenReturn(null);
         assertThrows(ServiceException.class, () -> service.logs("foreign",query)); verifyNoInteractions(logs);

+ 2 - 2
pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmReviewScopeServiceTest.java

@@ -23,7 +23,7 @@ class AlarmReviewScopeServiceTest {
     @BeforeEach void login() {
         SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
                 new LoginUser(9L, 10L, new SysUser(), Collections.emptySet()), null));
-        SysMenu menu = new SysMenu(); menu.setComponent("subSystem/alarm/audit"); menu.setStatus("0");
+        SysMenu menu = new SysMenu(); menu.setComponent("subSystem/alarm/common/audit"); menu.setStatus("0");
         menu.setQuery("{\"rootTypeId\":3,\"warning_code\":\"WARN-GAS-CONCENTRATION\"}");
         when(menus.selectMenuList(9L)).thenReturn(Collections.singletonList(menu));
         query.setRootTypeId(3); query.setWarningCode("WARN-GAS-CONCENTRATION");
@@ -39,7 +39,7 @@ class AlarmReviewScopeServiceTest {
     }
     @Test void missingRootDoesNotFallBackToUnrestrictedList() { when(types.selectList(null)).thenReturn(Collections.singletonList(type(22,3))); assertTrue(service.resolve(query).isEmpty()); }
     @Test void operationsMenuAllowsOnlyItsRootAndDoesNotGrantAudit() {
-        SysMenu menu = new SysMenu(); menu.setStatus("0"); menu.setComponent("subSystem/alarm/list");
+        SysMenu menu = new SysMenu(); menu.setStatus("0"); menu.setComponent("subSystem/alarm/common/list");
         menu.setQuery("{\"rootTypeId\":1}"); when(menus.selectMenuList(9L)).thenReturn(Collections.singletonList(menu));
         query.setRootTypeId(1); query.setWarningCode(null);
         assertEquals(Arrays.asList(1,12), service.resolveOperations(query));