Переглянути джерело

供水子系统-监测报警一张图整合

zengy 3 тижнів тому
батько
коміт
52940d1309
21 змінених файлів з 99 додано та 720 видалено
  1. 8 5
      docs/alarm-operations-2026-09-24.md
  2. 3 2
      docs/alarm-review-2026-09-23.md
  3. 1 1
      pipe-network-service/CONTEXT.md
  4. 2 2
      pipe-network-service/sql/migration_alarm_operations.sql
  5. 1 1
      pipe-network-service/sql/migration_alarm_review.sql
  6. 1 1
      pipe-network-service/sql/tests/alarm_review.test.mjs
  7. 1 67
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmController.java
  8. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java
  9. 0 89
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmLedgerControllerTest.java
  10. 0 54
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmMapPointOutDTO.java
  11. 0 44
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/IWaterSupplyAlarmService.java
  12. 2 203
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java
  13. 1 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmReviewQuery.java
  14. 7 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmOperationsService.java
  15. 4 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewScopeService.java
  16. 1 2
      pipe-network-service/zksy-system/src/main/resources/mapper/base/AlarmOperationsMapper.xml
  17. 0 234
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmLedgerServiceTest.java
  18. 0 2
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmRealtimeContractTest.java
  19. 9 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/mapper/AlarmOperationsMapperTest.java
  20. 43 2
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmOperationsServiceTest.java
  21. 14 2
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmReviewScopeServiceTest.java

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

@@ -2,15 +2,16 @@
 
 ## 交付入口
 
-后端统一在 `AlarmController`,查询服务/DTO/枚举/Mapper 位于 `zksy-system/base/alarm`。前端共用 `src/views/subSystem/alarm`。
+后端统一在 `AlarmController`,查询服务/DTO/枚举/Mapper 位于 `zksy-system/base/alarm`。前端公共报警页面及组件位于 `src/views/subSystem/alarm`,排水研判和 GIS 位于 `src/views/subSystem/drainage/alarm`,供水 GIS 位于 `src/views/subSystem/waterSupply/alarm`。
 
 | 既有菜单 | path | component | query |
 | --- | --- | --- | --- |
 | 2139 | alarmRemind | subSystem/alarm/remind | {"rootTypeId":2} |
 | 2140 | alarmList | subSystem/alarm/list | {"rootTypeId":2} |
-| 2141 | drainageAlarmAnalysis | subSystem/alarm/drainage/analysis | {"rootTypeId":2} |
-| 2143 | drainageAlarmGIS | subSystem/alarm/drainage/alarmGIS | {"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} |
+| 2120 | waterSupplyAlarmGIS | subSystem/waterSupply/alarm/alarmGIS | {"rootTypeId":1} |
 
 GIS 按最终要求命名为 `alarmGIS.vue`,没有“周边环境”页签;保留设备信息、报警记录、监测曲线。菜单更新不创建新页面菜单,不调整父级、排序和角色关联。
 
@@ -24,14 +25,16 @@ 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/index`,有清单权限不等于有审核权限。GIS/研判只接受排水根类型。
+所有新接口要求 `alarm:operations:list`,同时服务端按用户获授权的菜单校验 `rootTypeId` 和可选 `warningCode`,并查询设备类型根及全部后代。空设备类型范围返回空,不退化为无限制查询。审核授权仍单独校验 `subSystem/alarm/audit`,有清单权限不等于有审核权限。地图和曲线接口接受供水及排水根类型,独立研判页面仍只用于排水。
 
-2117 同时保留 `waterSupply:alarm:list`(框架支持逗号分隔的权限),避免仍使用原接口的供水首页和供水报警一张图失去查询权限。
+2117 同时保留 `waterSupply:alarm:list`(框架支持逗号分隔的权限),供供水首页的统计和实时异常接口使用。供水地图及首页报警点位已使用统一地图接口。
 
 监测曲线使用采集协议白名单:雷达流量/流速、水位、压力,以及水质悬浮物/COD/氨氮/电导率/pH。水质按 `device_id` 和 `node_id` 关联原采集数据。日/月均值直接聚合有效原始值,字符串无效值不强转、不补零,空日/月保留断点;原始曲线大于区间中位采样间隔 3 倍时断线。未配置固定采样周期,故不虚构覆盖率。状态类等没有数值数据源的报警显示无曲线。原始结果超过 20000 个时间点时提示改看日/月均值。
 
 ## 数据库脚本及部署
 
+页面目录调整的已部署环境执行 `pipe-network-service/sql/migration_alarm_menu_components.sql`:仅更新审核菜单 2118/2142/2097/2024 和排水研判/GIS 菜单 2141/2143 的 component,不改变 path、query 或权限;同步更新前后端后重新登录。供水 GIS 菜单 2120 单独执行 `migration_water_supply_alarm_gis.sql`。初装脚本已经使用最新组件路径。
+
 本轮脚本:`pipe-network-service/sql/migration_alarm_operations.sql`(KingbaseES PostgreSQL 兼容模式)。首次部署顺序:`migration_alarm_case_work_order_id.sql` → `migration_alarm_review.sql` → `migration_alarm_operations.sql`,先落地 `work_order_id`/`create_time` 字段,再创建日志与菜单。各脚本均不再创建生命周期触发器或自定义函数。
 
 已部署上述表结构的环境,本次必须重新部署管理端及 `zk-api-service`,日志已经改为业务代码主动写入,不依赖安装日志触发器。`pipe-network-service/sql/migration_alarm_lifecycle_log.sql` 作为增量补丁,更新单号注释/唯一索引、清除旧数据库赋号机制,并删除旧报警生命周期、工单日志镜像触发器及对应自定义函数,可重复执行,不更新菜单、不补录历史日志。部署时停止相关写入,执行清理脚本并同步更新管理端和采集端,再恢复服务。此后直接 SQL 修改业务表不会自动产生操作日志,应通过业务服务入口操作。

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

@@ -2,7 +2,7 @@
 
 ## 已确认契约
 
-- 前端 `city-life-line/src/views/subSystem/alarm/index.vue`,后端 `AlarmController`,独立的审核服务位于 `base/alarm`。
+- 前端 `city-life-line/src/views/subSystem/alarm/audit.vue`,后端 `AlarmController`,独立的审核服务位于 `base/alarm`。
 - 仅 `alarm_status=0` 进入审核列表,一条事件一行,不按设备去重;后端分页和时间排序。
 - `audit_status`:0 未审核、1 确认报警、2 误报警。`alarm_level` 继续使用 1~4(一级最严重,红/橙/蓝/绿),不新增或更名为 `case_level`。
 - 确认报警原子更新审核状态、记录审核日志并创建工单;任一步失败整个审核事务回滚。不修改设备状态,也不将报警标为已处理。误报警仅记录结论及意见。
@@ -20,7 +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/index'`。设备类型根分别为 1/2/3/4,燃气额外 `warning_code='WARN-GAS-CONCENTRATION'`;保留原父级和角色关联。
+- 精准更新已有菜单 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`,同步更新前后端后重新登录,无需重复执行建表或权限初始化。
 - 页面权限为 `alarm:review:list`;仅在缺失时创建 `alarm:review:audit` 按钮权限,给对应页面的现有角色补充新按钮授权,重复执行不恢复已经撤销的按钮授权。不会重复新增四个页面菜单。
 - `rootTypeId` 代表 `equipment_type.id` 的根,查询包含所有后代;服务端校验当前用户实际获授权的菜单范围,不能仅修改前端 query 跨系统访问。
 

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

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

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

@@ -26,10 +26,10 @@ 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'
-        WHEN 2141 THEN 'subSystem/alarm/drainage/analysis' WHEN 2143 THEN 'subSystem/alarm/drainage/alarmGIS'
+        WHEN 2141 THEN 'subSystem/drainage/alarm/analysis' WHEN 2143 THEN 'subSystem/drainage/alarm/alarmGIS'
         WHEN 2117 THEN 'subSystem/alarm/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'
                  ELSE 'alarm:operations:list' END,
     update_time = CURRENT_TIMESTAMP

+ 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/index', perms = 'alarm:review:list',
+UPDATE app_user.sys_menu SET path = 'alarmAudit', component = 'subSystem/alarm/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/index')
+    assert.equal(menu.path, 'alarmAudit'); assert.equal(menu.component, 'subSystem/alarm/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')

+ 1 - 67
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmController.java

@@ -1,7 +1,5 @@
 package com.zksy.web.controller.WaterSupply.MonitorAlarm;
 
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyAlarmService;
@@ -16,16 +14,14 @@ import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
-import java.time.LocalDateTime;
 import java.util.List;
 
 /**
  * 供水管网-监测报警接口
- * 保留供水首页统计、实时异常及供水监测报警一张图查询;清单和审核使用统一 AlarmController
+ * 保留供水首页统计、实时异常与清警复核;地图、清单、详情和审核使用统一 AlarmController
  */
 @Slf4j
 @RestController
@@ -68,66 +64,4 @@ public class WaterSupplyAlarmController {
         List<WaterAlarmOutDTO> list = waterSupplyAlarmService.getRealtimeAlarmList();
         return AjaxResult.success(list);
     }
-
-    @GetMapping("/history")
-    @ApiOperation("历史异常信息分页查询(支持按时间、设备、预警类型、等级、状态查询)")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
-    public AjaxResult getHistoryAlarmPage(
-            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam(value = "每页数量", defaultValue = "10") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam(value = "设备编码") @RequestParam(required = false) String deviceCode,
-            @ApiParam(value = "预警类型") @RequestParam(required = false) String warningType,
-            @ApiParam(value = "报警等级 1-4") @RequestParam(required = false) Integer alarmLevel,
-            @ApiParam(value = "报警状态 0-未处理 1-已处理") @RequestParam(required = false) Integer alarmStatus,
-            @ApiParam(value = "开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false)
-            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
-            @ApiParam(value = "结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false)
-            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
-            @ApiParam(value = "关键字(设备编码/预警类型/预警编码)") @RequestParam(required = false) String keyword) {
-        Page<WaterAlarmOutDTO> page = waterSupplyAlarmService.getHistoryAlarmPage(
-                pageNum, pageSize, deviceCode, warningType, alarmLevel, alarmStatus, startTime, endTime, keyword);
-        return AjaxResult.success(page);
-    }
-
-    @GetMapping("/detail/{alarmId}")
-    @ApiOperation("异常详情(报警数据 + 设备数据 + 关联工单)")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
-    public AjaxResult getAlarmDetail(@PathVariable String alarmId) {
-        WaterAlarmOutDTO detail = waterSupplyAlarmService.getAlarmDetail(alarmId);
-        return detail != null ? AjaxResult.success(detail) : AjaxResult.error("报警信息不存在");
-    }
-
-    // ------------------------------------------------------------------
-    // 报警管理只读台账(T02)
-    // ------------------------------------------------------------------
-
-    @GetMapping("/ledger/page")
-    @ApiOperation("报警管理只读台账分页(仅供水设备,展示状态后端推导)")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
-    public AjaxResult getAlarmLedgerPage(
-            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam(value = "每页数量", defaultValue = "10") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam("设备编码") @RequestParam(required = false) String deviceCode,
-            @ApiParam("报警类型") @RequestParam(required = false) String warningType,
-            @ApiParam("报警等级 1-4") @RequestParam(required = false) Integer alarmLevel,
-            @ApiParam("报警展示状态 1-待审核 2-已转工单 3-已解除") @RequestParam(required = false) Integer displayStatus,
-            @ApiParam("设备报警状态 0-正常 1-报警") @RequestParam(required = false) Integer deviceAlarmStatus,
-            @ApiParam(value = "开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false)
-            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
-            @ApiParam(value = "结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false)
-            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
-            @ApiParam("关键字(设备编码/报警类型/预警编码)") @RequestParam(required = false) String keyword) {
-        Page<WaterAlarmOutDTO> page = waterSupplyAlarmService.getAlarmLedgerPage(
-                pageNum, pageSize, deviceCode, warningType, alarmLevel, displayStatus,
-                deviceAlarmStatus, startTime, endTime, keyword);
-        return AjaxResult.success(page);
-    }
-
-    @GetMapping("/map/points")
-    @ApiOperation("监测报警一张图-报警点位(按报警级别着色)")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
-    public AjaxResult getAlarmMapPoints() {
-        List<AlarmMapPointOutDTO> points = waterSupplyAlarmService.getAlarmMapPoints();
-        return AjaxResult.success(points);
-    }
 }

+ 1 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java

@@ -74,7 +74,7 @@ public class AlarmController {
     public AjaxResult options(@Valid AlarmQuery query) { return AjaxResult.success(operations.options(query)); }
 
     @GetMapping("/map")
-    @ApiOperation("排水报警GIS点位")
+    @ApiOperation("供水、排水当前报警GIS点位,按设备大类隔离")
     @PreAuthorize("@ss.hasPermi('alarm:operations:list')")
     public AjaxResult map(@Valid AlarmQuery query) { return AjaxResult.success(operations.map(query)); }
 

+ 0 - 89
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmLedgerControllerTest.java

@@ -1,89 +0,0 @@
-package com.zksy.web.controller.WaterSupply.MonitorAlarm;
-
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyAlarmService;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.test.util.ReflectionTestUtils;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-
-import java.util.Collections;
-import java.util.List;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-/**
- * 供水报警管理只读台账契约测试(T02 / Issue #45)。
- *
- * <p>被测公共 seam:{@code GET /waterSupply/alarm/ledger/page}。
- * 只验证外部可见行为:响应字段与筛选条件透传。</p>
- */
-class WaterSupplyAlarmLedgerControllerTest {
-
-    private IWaterSupplyAlarmService alarmService;
-    private MockMvc mockMvc;
-
-    @BeforeEach
-    void setUp() {
-        alarmService = mock(IWaterSupplyAlarmService.class);
-        WaterSupplyAlarmController controller = new WaterSupplyAlarmController();
-        ReflectionTestUtils.setField(controller, "waterSupplyAlarmService", alarmService);
-        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
-    }
-
-    /**
-     * MockMvc 默认以 ISO-8859-1 解码响应,这里显式按 UTF-8 取体,便于断言中文字段。
-     */
-    private static String body(org.springframework.test.web.servlet.MvcResult result) throws Exception {
-        return new String(result.getResponse().getContentAsByteArray(), java.nio.charset.StandardCharsets.UTF_8);
-    }
-
-    @Test
-    void ledgerPageExposesDerivedDisplayStatusAndDeviceAlarmStatus() throws Exception {
-        WaterAlarmOutDTO row = new WaterAlarmOutDTO();
-        row.setId("alarm-1");
-        row.setDeviceCode("CN-GS-202609020001");
-        row.setWarningType("压力预警");
-        row.setWarningCode("WARN-PRESSURE");
-        row.setAlarmLevel(1);
-        row.setAlarmLevelText("一级报警");
-        row.setDeviceAlarmStatus(1);
-        row.setDeviceAlarmStatusText("报警中");
-        row.setDisplayStatus(2);
-        row.setDisplayStatusText("已转工单");
-        row.setOrderNo("WO202606101703482640");
-        Page<WaterAlarmOutDTO> page = new Page<>(1, 10, 1);
-        page.setRecords(Collections.singletonList(row));
-        when(alarmService.getAlarmLedgerPage(anyLong(), anyLong(), any(), any(), any(), any(), any(), any(), any(), any()))
-                .thenReturn(page);
-
-        mockMvc.perform(get("/waterSupply/alarm/ledger/page")
-                        .param("pageNum", "1")
-                        .param("pageSize", "10"))
-                .andExpect(status().isOk())
-                .andExpect(result -> {
-                    String json = body(result);
-                    assertThat(json, "\"displayStatus\":2");
-                    assertThat(json, "\"displayStatusText\":\"已转工单\"");
-                    assertThat(json, "\"deviceAlarmStatus\":1");
-                    assertThat(json, "\"deviceAlarmStatusText\":\"报警中\"");
-                    assertThat(json, "WO202606101703482640");
-                    assertThat(json, "WARN-PRESSURE");
-                });
-    }
-
-    private static void assertThat(String json, String expected) {
-        if (!json.contains(expected)) {
-            throw new AssertionError("响应缺少期望内容:" + expected + "\n实际:" + json);
-        }
-    }
-}

+ 0 - 54
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmMapPointOutDTO.java

@@ -1,54 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm.dto.out;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-
-/**
- * 监测报警一张图-点位出参
- */
-@Data
-@ApiModel(value = "监测报警一张图-点位出参", description = "报警点位信息,供 GIS 地图按报警级别着色展示")
-public class AlarmMapPointOutDTO implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @ApiModelProperty(value = "报警ID")
-    private String alarmId;
-
-    @ApiModelProperty(value = "设备编码")
-    private String deviceCode;
-
-    @ApiModelProperty(value = "设备名称")
-    private String deviceName;
-
-    @ApiModelProperty(value = "设备位置")
-    private String equipmentLocation;
-
-    @ApiModelProperty(value = "经度")
-    private String longitude;
-
-    @ApiModelProperty(value = "纬度")
-    private String latitude;
-
-    @ApiModelProperty(value = "预警类型")
-    private String warningType;
-
-    @ApiModelProperty(value = "报警参数值")
-    private BigDecimal actualValue;
-
-    @ApiModelProperty(value = "报警等级:1-4,1级最高(用于地图着色)")
-    private Integer alarmLevel;
-
-    @ApiModelProperty(value = "报警等级文本")
-    private String alarmLevelText;
-
-    @ApiModelProperty(value = "报警触发时间(取记录创建时间)")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime createTime;
-}

+ 0 - 44
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/IWaterSupplyAlarmService.java

@@ -1,11 +1,8 @@
 package com.zksy.WaterSupply.MonitorAlarm.service;
 
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
 
-import java.time.LocalDateTime;
 import java.util.List;
 
 /**
@@ -22,45 +19,4 @@ public interface IWaterSupplyAlarmService {
      * 实时异常报警列表(未处理报警,信息卡片展示)
      */
     List<WaterAlarmOutDTO> getRealtimeAlarmList();
-
-    /**
-     * 历史异常报警分页查询
-     */
-    Page<WaterAlarmOutDTO> getHistoryAlarmPage(long pageNum, long pageSize,
-                                               String deviceCode, String warningType,
-                                               Integer alarmLevel, Integer alarmStatus,
-                                               LocalDateTime startTime, LocalDateTime endTime,
-                                               String keyword);
-
-    /**
-     * 报警管理只读台账分页:仅供水设备报警,展示状态由后端推导
-     *
-     * @param displayStatus     报警展示状态:1-待审核 2-已转工单 3-已解除
-     * @param deviceAlarmStatus 设备报警状态:0-正常 1-报警
-     */
-    Page<WaterAlarmOutDTO> getAlarmLedgerPage(long pageNum, long pageSize,
-                                              String deviceCode, String warningType,
-                                              Integer alarmLevel, Integer displayStatus,
-                                              Integer deviceAlarmStatus,
-                                              LocalDateTime startTime, LocalDateTime endTime,
-                                              String keyword);
-
-    /**
-     * 报警管理只读台账导出行(与分页同一推导口径,不分页)
-     */
-    List<WaterAlarmOutDTO> getAlarmLedgerExport(String deviceCode, String warningType,
-                                                Integer alarmLevel, Integer displayStatus,
-                                                Integer deviceAlarmStatus,
-                                                LocalDateTime startTime, LocalDateTime endTime,
-                                                String keyword);
-
-    /**
-     * 报警详情(报警数据 + 设备数据 + 关联工单)
-     */
-    WaterAlarmOutDTO getAlarmDetail(String alarmId);
-
-    /**
-     * 监测报警一张图点位(未处理报警,含经纬度与报警级别)
-     */
-    List<AlarmMapPointOutDTO> getAlarmMapPoints();
 }

+ 2 - 203
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java

@@ -4,8 +4,6 @@ import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyAlarmService;
@@ -22,9 +20,6 @@ import com.zksy.base.domain.EquipmentStatus;
 import com.zksy.base.domain.EquipmentType;
 import com.zksy.base.mapper.EquipmentTypeMapper;
 import com.zksy.ops.domain.WorkOrder;
-import com.zksy.base.mapper.EquipmentBaseMapper;
-import com.zksy.ops.mapper.WorkOrderMapper;
-import com.zksy.common.exception.ServiceException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -34,8 +29,6 @@ import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -60,14 +53,6 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     @Autowired
     private WaterSupplyQueryService waterSupplyQueryService;
 
-    @Autowired
-    private EquipmentBaseMapper equipmentBaseMapper;
-
-    @Autowired
-    private WorkOrderMapper workOrderMapper;
-
-
-
     @Autowired
     private EquipmentTypeMapper equipmentTypeMapper;
 
@@ -103,7 +88,7 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     }
 
     /**
-     * 报警ID -> 工单映射(通过 alarm_data.order_id 关联)
+     * 报警ID -> 工单映射(通过 alarm_data.work_order_id 关联)
      */
     private Map<String, WorkOrder> loadWorkOrderMapByAlarmIds(List<String> alarmIds) {
         return waterSupplyQueryService.latestOrdersByAlarmIds(alarmIds);
@@ -151,7 +136,7 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     }
 
     // ------------------------------------------------------------------
-    // 实时 / 历史 / 详情
+    // 首页实时异常
     // ------------------------------------------------------------------
 
     @Override
@@ -172,99 +157,6 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
                 .collect(Collectors.toList());
     }
 
-    @Override
-    public Page<WaterAlarmOutDTO> getHistoryAlarmPage(long pageNum, long pageSize,
-                                                      String deviceCode, String warningType,
-                                                      Integer alarmLevel, Integer alarmStatus,
-                                                      LocalDateTime startTime, LocalDateTime endTime,
-                                                      String keyword) {
-        Page<AlarmData> page = new Page<>(pageNum, pageSize);
-        Set<String> waterCodes = resolveWaterDeviceCodes();
-        LambdaQueryWrapper<AlarmData> wrapper = new LambdaQueryWrapper<>();
-        if (!waterCodes.isEmpty()) {
-            wrapper.in(AlarmData::getDeviceCode, waterCodes);
-        } else {
-            wrapper.eq(AlarmData::getId, "-1");
-        }
-        wrapper.eq(StrUtil.isNotBlank(deviceCode), AlarmData::getDeviceCode, deviceCode)
-                .like(StrUtil.isNotBlank(warningType), AlarmData::getWarningType, warningType)
-                .eq(alarmLevel != null, AlarmData::getAlarmLevel, alarmLevel)
-                .eq(alarmStatus != null, AlarmData::getAlarmStatus, alarmStatus)
-                .ge(startTime != null, AlarmData::getCreateTime, startTime)
-                .le(endTime != null, AlarmData::getCreateTime, endTime)
-                .and(StrUtil.isNotBlank(keyword), w -> w.like(AlarmData::getDeviceCode, keyword)
-                        .or().like(AlarmData::getWarningType, keyword)
-                        .or().like(AlarmData::getWarningCode, keyword))
-                .orderByDesc(AlarmData::getCreateTime);
-        Page<AlarmData> result = alarmDataService.page(page, wrapper);
-        Page<WaterAlarmOutDTO> outPage = new Page<>(pageNum, pageSize, result.getTotal());
-        outPage.setRecords(convertToVoList(result.getRecords()));
-        return outPage;
-    }
-
-    @Override
-    public WaterAlarmOutDTO getAlarmDetail(String alarmId) {
-        AlarmData alarm = alarmDataService.getById(alarmId);
-        if (alarm == null) {
-            throw new ServiceException("报警信息不存在");
-        }
-        List<WaterAlarmOutDTO> vos = convertToVoList(Collections.singletonList(alarm));
-        return vos.isEmpty() ? null : vos.get(0);
-    }
-
-    // ------------------------------------------------------------------
-    // 报警管理只读台账(供水设备 + 后端推导展示状态)
-    // ------------------------------------------------------------------
-
-    @Override
-    public Page<WaterAlarmOutDTO> getAlarmLedgerPage(long pageNum, long pageSize,
-                                                     String deviceCode, String warningType,
-                                                     Integer alarmLevel, Integer displayStatus,
-                                                     Integer deviceAlarmStatus,
-                                                     LocalDateTime startTime, LocalDateTime endTime,
-                                                     String keyword) {
-        List<WaterAlarmOutDTO> rows = getAlarmLedgerExport(deviceCode, warningType, alarmLevel,
-                displayStatus, deviceAlarmStatus, startTime, endTime, keyword);
-        Page<WaterAlarmOutDTO> page = new Page<>(pageNum, pageSize, rows.size());
-        if (pageNum < 1 || pageSize < 1) {
-            page.setRecords(new ArrayList<>());
-            return page;
-        }
-        int from = (int) Math.min((pageNum - 1) * pageSize, rows.size());
-        int to = (int) Math.min(from + pageSize, rows.size());
-        page.setRecords(new ArrayList<>(rows.subList(from, to)));
-        return page;
-    }
-
-    @Override
-    public List<WaterAlarmOutDTO> getAlarmLedgerExport(String deviceCode, String warningType,
-                                                       Integer alarmLevel, Integer displayStatus,
-                                                       Integer deviceAlarmStatus,
-                                                       LocalDateTime startTime, LocalDateTime endTime,
-                                                       String keyword) {
-        Set<String> waterCodes = resolveWaterDeviceCodes();
-        if (waterCodes.isEmpty()) {
-            return new ArrayList<>();
-        }
-        LambdaQueryWrapper<AlarmData> wrapper = new LambdaQueryWrapper<AlarmData>()
-                .in(AlarmData::getDeviceCode, waterCodes)
-                .eq(StrUtil.isNotBlank(deviceCode), AlarmData::getDeviceCode, deviceCode)
-                .like(StrUtil.isNotBlank(warningType), AlarmData::getWarningType, warningType)
-                .eq(alarmLevel != null, AlarmData::getAlarmLevel, alarmLevel)
-                .ge(startTime != null, AlarmData::getCreateTime, startTime)
-                .le(endTime != null, AlarmData::getCreateTime, endTime)
-                .and(StrUtil.isNotBlank(keyword), w -> w.like(AlarmData::getDeviceCode, keyword)
-                        .or().like(AlarmData::getWarningType, keyword)
-                        .or().like(AlarmData::getWarningCode, keyword))
-                .orderByDesc(AlarmData::getCreateTime);
-        List<AlarmData> alarms = alarmDataService.list(wrapper);
-        List<WaterAlarmOutDTO> rows = buildLedgerVoList(alarms);
-        return rows.stream()
-                .filter(row -> displayStatus == null || displayStatus.equals(row.getDisplayStatus()))
-                .filter(row -> deviceAlarmStatus == null || deviceAlarmStatus.equals(row.getDeviceAlarmStatus()))
-                .collect(Collectors.toList());
-    }
-
     /**
      * 报警记录 -> 台账出参:补充设备报警状态与后端推导的报警展示状态。
      *
@@ -364,101 +256,8 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
         return waterSupplyQueryService.statusesByDeviceIds(equipmentIds);
     }
 
-    /**
-     * 报警记录 -> 出参(填充设备名称/位置/经纬度及关联工单)
-     */
-    private List<WaterAlarmOutDTO> convertToVoList(List<AlarmData> alarms) {
-        if (CollUtil.isEmpty(alarms)) {
-            return new ArrayList<>();
-        }
-        Map<String, EquipmentBase> eqMap = loadEquipmentCodeMap(alarms.stream()
-                .map(AlarmData::getDeviceCode).collect(Collectors.toSet()));
-        Map<Integer, String> typeNames = loadDeviceTypeNames(eqMap.values());
-        List<String> alarmIds = alarms.stream().map(AlarmData::getId).collect(Collectors.toList());
-        Map<String, WorkOrder> orderMap = loadWorkOrderMapByAlarmIds(alarmIds);
-        List<WaterAlarmOutDTO> vos = new ArrayList<>();
-        for (AlarmData alarm : alarms) {
-            WaterAlarmOutDTO vo = BeanUtil.copyProperties(alarm, WaterAlarmOutDTO.class);
-            EquipmentBase eq = alarm.getDeviceCode() == null ? null : eqMap.get(alarm.getDeviceCode());
-            if (eq != null) {
-                vo.setEquipmentId(eq.getEquipmentId());
-                vo.setDeviceType(typeNames.get(eq.getEquipmentTypeId()));
-                if (StrUtil.isBlank(vo.getDeviceName())) {
-                    vo.setDeviceName(eq.getEquipmentName());
-                }
-                vo.setEquipmentLocation(eq.getEquipmentLocation());
-                if (eq.getLongitude() != null) {
-                    vo.setLongitude(eq.getLongitude().toPlainString());
-                }
-                if (eq.getLatitude() != null) {
-                    vo.setLatitude(eq.getLatitude().toPlainString());
-                }
-            }
-            vo.setAlarmLevelText(formatAlarmLevel(alarm.getAlarmLevel()));
-            vo.setAlarmStatusText(alarm.getAlarmStatus() != null && alarm.getAlarmStatus() == 0 ? "待处理" : "已处理");
-            WorkOrder order = alarm.getId() == null ? null : orderMap.get(alarm.getId());
-            if (order != null) {
-                vo.setOrderNo(order.getOrderNo());
-                vo.setOrderStatus(order.getOrderStatus());
-            }
-            vos.add(vo);
-        }
-        return vos;
-    }
-
     private String formatAlarmLevel(Integer alarmLevel) {
         AlarmLevelEnum level = AlarmLevelEnum.fromCode(alarmLevel);
         return level == null ? "未知等级" : level.getLabel();
     }
-
-    // ------------------------------------------------------------------
-    // 监测报警一张图
-    // ------------------------------------------------------------------
-
-    @Override
-    public List<AlarmMapPointOutDTO> getAlarmMapPoints() {
-        Set<String> waterCodes = resolveWaterDeviceCodes();
-        if (waterCodes.isEmpty()) {
-            return new ArrayList<>();
-        }
-        List<AlarmData> pendingAlarms = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
-                .in(AlarmData::getDeviceCode, waterCodes)
-                .eq(AlarmData::getAlarmStatus, 0)
-                .orderByDesc(AlarmData::getCreateTime));
-        Map<String, EquipmentBase> eqMap = loadEquipmentCodeMap(pendingAlarms.stream()
-                .map(AlarmData::getDeviceCode).collect(Collectors.toSet()));
-        // 同一设备只展示最新一条未处理报警
-        Map<String, AlarmData> latestPerDevice = new LinkedHashMap<>();
-        for (AlarmData alarm : pendingAlarms) {
-            latestPerDevice.putIfAbsent(alarm.getDeviceCode(), alarm);
-        }
-        List<AlarmMapPointOutDTO> points = new ArrayList<>();
-        for (AlarmData alarm : latestPerDevice.values()) {
-            EquipmentBase eq = alarm.getDeviceCode() == null ? null : eqMap.get(alarm.getDeviceCode());
-            if (eq == null || eq.getLongitude() == null || eq.getLatitude() == null) {
-                continue;
-            }
-            AlarmMapPointOutDTO point = new AlarmMapPointOutDTO();
-            point.setAlarmId(alarm.getId());
-            point.setDeviceCode(alarm.getDeviceCode());
-            point.setDeviceName(eq.getEquipmentName());
-            point.setEquipmentLocation(eq.getEquipmentLocation());
-            point.setLongitude(eq.getLongitude().toPlainString());
-            point.setLatitude(eq.getLatitude().toPlainString());
-            point.setWarningType(alarm.getWarningType());
-            point.setActualValue(alarm.getActualValue());
-            point.setAlarmLevel(alarm.getAlarmLevel());
-            point.setAlarmLevelText(formatAlarmLevel(alarm.getAlarmLevel()));
-            point.setCreateTime(alarm.getCreateTime());
-            points.add(point);
-        }
-        points.sort(Comparator.comparing(AlarmMapPointOutDTO::getAlarmLevel, Comparator.nullsLast(Comparator.naturalOrder())));
-        return points;
-    }
-
-    // ------------------------------------------------------------------
-    // 当前用户
-    // ------------------------------------------------------------------
-
-
 }

+ 1 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmReviewQuery.java

@@ -14,6 +14,7 @@ public class AlarmReviewQuery {
     @ApiModelProperty("报警编码,精确匹配;燃气菜单限定 WARN-GAS-CONCENTRATION")
     @Size(max = 100)
     private String warningCode;
+    @ApiModelProperty("设备编码/名称,模糊匹配")
     @Size(max = 100)
     private String keyword;
     private Integer alarmLevel;

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

@@ -88,7 +88,8 @@ public class AlarmOperationsService {
         return result;
     }
     public List<AlarmVO> map(AlarmQuery query) {
-        requireDrainage(query);
+        requireWaterSubsystem(query);
+        query.setAlarmStatus(AlarmRecordStatusEnum.UNHANDLED.getCode());
         validate(query);
         List<AlarmVO> result = mapper.selectMap(query, scope.resolveOperations(query));
         if (result.size() > 10000) throw new ServiceException("报警数量超过地图展示上限,请缩小查询范围");
@@ -96,7 +97,7 @@ public class AlarmOperationsService {
         return result;
     }
     public Map<String, Object> analysis(String id, AlarmQuery query, AlarmSeriesPeriod period) {
-        requireDrainage(query);
+        requireWaterSubsystem(query);
         AlarmVO alarm = detail(id, query);
         AlarmMetricSource source = AlarmMetricSource.fromCode(alarm.getWarningCode());
         Map<String, Object> result = new LinkedHashMap<>();
@@ -151,9 +152,10 @@ public class AlarmOperationsService {
     private Map<String, Object> option(int value, String label) {
         Map<String,Object> result = new LinkedHashMap<>(); result.put("value",value); result.put("label",label); return result;
     }
-    private void requireDrainage(AlarmQuery query) {
-        if (!EquipmentTypeEnum.ROOT_DRAINAGE_DEVICE.getId().equals(query.getRootTypeId()))
-            throw new ServiceException("该功能仅适用于排水子系统");
+    private void requireWaterSubsystem(AlarmQuery query) {
+        if (!EquipmentTypeEnum.ROOT_WATER_SUPPLY_DEVICE.getId().equals(query.getRootTypeId())
+                && !EquipmentTypeEnum.ROOT_DRAINAGE_DEVICE.getId().equals(query.getRootTypeId()))
+            throw new ServiceException("该功能仅适用于供水或排水子系统");
     }
     private void validate(AlarmQuery query) {
         if (query.getPageNum() < 1 || query.getPageSize() < 1 || query.getPageSize() > 100) throw new ServiceException("分页参数无效");

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

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

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

@@ -23,8 +23,7 @@
         <include refid="com.zksy.base.alarm.mapper.AlarmReviewMapper.scope"/>
         <if test="query.keyword != null and query.keyword != ''">
             AND (a.device_code ILIKE CONCAT('%',#{query.keyword},'%')
-                 OR e.equipment_name ILIKE CONCAT('%',#{query.keyword},'%')
-                 OR e.equipment_location ILIKE CONCAT('%',#{query.keyword},'%'))
+                 OR e.equipment_name ILIKE CONCAT('%',#{query.keyword},'%'))
         </if>
         <if test="query.alarmNo != null and query.alarmNo != ''">AND a.alarm_no ILIKE CONCAT('%',#{query.alarmNo},'%')</if>
         <if test="query.deviceCode != null and query.deviceCode != ''">AND a.device_code = #{query.deviceCode}</if>

+ 0 - 234
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmLedgerServiceTest.java

@@ -1,234 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm;
-
-import com.baomidou.mybatisplus.core.MybatisConfiguration;
-import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.service.impl.WaterSupplyAlarmServiceImpl;
-import com.zksy.base.service.impl.WaterSupplyQueryService;
-import com.zksy.base.alarm.domain.AlarmData;
-import com.zksy.base.alarm.mapper.AlarmDataMapper;
-import com.zksy.base.alarm.service.AlarmDataService;
-import com.zksy.base.domain.EquipmentBase;
-import com.zksy.base.domain.EquipmentStatus;
-import com.zksy.base.mapper.EquipmentBaseMapper;
-import com.zksy.base.mapper.EquipmentStatusMapper;
-import com.zksy.base.mapper.EquipmentTypeMapper;
-import com.zksy.base.service.EquipmentBaseService;
-import com.zksy.ops.domain.WorkOrder;
-import com.zksy.ops.mapper.WorkOrderMapper;
-import org.apache.ibatis.builder.MapperBuilderAssistant;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * 报警管理只读台账推导口径测试(T02 / Issue #45)。
- *
- * <p>被测公共 seam:{@link com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyAlarmService#getAlarmLedgerPage}。
- * 验证报警展示状态、设备报警状态与供水数据边界三项外部可见行为。</p>
- */
-class WaterSupplyAlarmLedgerServiceTest {
-
-    @BeforeAll
-    static void initLambdaCache() {
-        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
-        TableInfoHelper.initTableInfo(assistant, AlarmData.class);
-        TableInfoHelper.initTableInfo(assistant, EquipmentBase.class);
-        TableInfoHelper.initTableInfo(assistant, EquipmentStatus.class);
-        TableInfoHelper.initTableInfo(assistant, WorkOrder.class);
-    }
-
-    private WaterSupplyAlarmServiceImpl service;
-    private AlarmDataService alarmDataService;
-    private AlarmDataMapper alarmDataMapper;
-    private EquipmentBaseService equipmentBaseService;
-    private EquipmentBaseMapper equipmentBaseMapper;
-    private EquipmentStatusMapper equipmentStatusMapper;
-    private EquipmentTypeMapper equipmentTypeMapper;
-    private WorkOrderMapper workOrderMapper;
-
-    private EquipmentBase pressureDevice;
-    private EquipmentBase flowDevice;
-
-    @BeforeEach
-    void setUp() {
-        service = new WaterSupplyAlarmServiceImpl();
-        alarmDataService = mock(AlarmDataService.class);
-        alarmDataMapper = mock(AlarmDataMapper.class);
-        equipmentBaseService = mock(EquipmentBaseService.class);
-        equipmentBaseMapper = mock(EquipmentBaseMapper.class);
-        equipmentStatusMapper = mock(EquipmentStatusMapper.class);
-        equipmentTypeMapper = mock(EquipmentTypeMapper.class);
-        workOrderMapper = mock(WorkOrderMapper.class);
-        ReflectionTestUtils.setField(service, "alarmDataService", alarmDataService);
-        ReflectionTestUtils.setField(service, "waterSupplyQueryService",
-                new WaterSupplyQueryService(equipmentBaseService, equipmentBaseMapper,
-                        equipmentStatusMapper, equipmentTypeMapper, workOrderMapper,
-                        alarmDataMapper));
-        ReflectionTestUtils.setField(service, "equipmentBaseMapper", equipmentBaseMapper);
-        ReflectionTestUtils.setField(service, "workOrderMapper", workOrderMapper);
-
-        pressureDevice = device("EQ-P-1", "CN-GS-202609020001", "城南压力监测点");
-        flowDevice = device("EQ-F-1", "CN-GS-202609020002", "城北流量监测点");
-        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Arrays.asList(pressureDevice, flowDevice));
-        when(equipmentBaseMapper.selectList(any())).thenReturn(Arrays.asList(pressureDevice, flowDevice));
-    }
-
-    private static EquipmentBase device(String id, String code, String name) {
-        EquipmentBase device = new EquipmentBase();
-        device.setEquipmentId(id);
-        device.setEquipmentCode(code);
-        device.setEquipmentName(name);
-        device.setEquipmentLocation("测试大道1号");
-        device.setLongitude(new BigDecimal("113.28"));
-        device.setLatitude(new BigDecimal("23.12"));
-        return device;
-    }
-
-    private static AlarmData alarm(String id, String deviceCode, String warningType, Integer alarmStatus,
-                                   Integer alarmLevel) {
-        AlarmData alarm = new AlarmData();
-        alarm.setId(id);
-        alarm.setDeviceCode(deviceCode);
-        alarm.setWarningType(warningType);
-        alarm.setWarningCode("WARN-" + warningType);
-        alarm.setAlarmStatus(alarmStatus);
-        alarm.setAlarmLevel(alarmLevel);
-        return alarm;
-    }
-
-    private void stubLedger(AlarmData... alarms) {
-        when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(Arrays.asList(alarms));
-    }
-
-    @SuppressWarnings("unchecked")
-    private Page<WaterAlarmOutDTO> ledgerPage() {
-        return service.getAlarmLedgerPage(1, 10, null, null, null, null, null, null, null, null);
-    }
-
-    @Test
-    void alarmDetailLoadsItsDeviceWithoutAnUnboundedEquipmentRead() {
-        AlarmData linkedAlarm = alarm("alarm-1", "CN-GS-202609020001", "压力预警", 1, 1);
-        linkedAlarm.setWorkOrderId(501L);
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(linkedAlarm));
-        when(alarmDataService.getById("alarm-1")).thenReturn(linkedAlarm);
-        // 数据访问边界拒绝无条件设备扫描,详情应只读取本条报警涉及的设备。
-        when(equipmentBaseMapper.selectList(isNull()))
-                .thenThrow(new IllegalStateException("禁止无条件扫描设备表"));
-        WorkOrder closed = new WorkOrder();
-        closed.setOrderId(501L);
-        closed.setOrderNo("WO-CLOSED");
-        closed.setOrderStatus(5);
-        when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(closed));
-
-        WaterAlarmOutDTO row = service.getAlarmDetail("alarm-1");
-
-        assertEquals("EQ-P-1", row.getEquipmentId());
-        assertEquals("城南压力监测点", row.getDeviceName());
-        assertEquals("WO-CLOSED", row.getOrderNo());
-        assertEquals(5, row.getOrderStatus());
-    }
-
-    @Test
-    void pendingAlarmWithoutAuditOrOrderShowsPendingAudit() {
-        stubLedger(alarm("alarm-1", "CN-GS-202609020001", "压力预警", 0, 1));
-        when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
-                status("EQ-P-1", 1, 1, 0)));
-
-        List<WaterAlarmOutDTO> rows = ledgerPage().getRecords();
-
-        assertEquals(1, rows.size());
-        WaterAlarmOutDTO row = rows.get(0);
-        assertEquals(1, row.getDisplayStatus());
-        assertEquals("待审核", row.getDisplayStatusText());
-        assertEquals(1, row.getDeviceAlarmStatus());
-        assertEquals("报警中", row.getDeviceAlarmStatusText());
-    }
-
-    @Test
-    void alarmWithLiveWorkOrderShowsTransferred() {
-        stubLedger(alarm("alarm-1", "CN-GS-202609020001", "压力预警", 1, 1));
-        AlarmData linkedAlarm = alarm("alarm-1", "CN-GS-202609020001", "压力过低", 0, 1);
-        linkedAlarm.setWorkOrderId(201L);
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(linkedAlarm));
-        when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(Collections.singletonList(linkedAlarm));
-        WorkOrder order = new WorkOrder();
-        order.setOrderId(201L);
-        order.setOrderNo("WO202606101703482640");
-        order.setOrderStatus(3);
-        when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(order));
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
-                status("EQ-P-1", 1, 1, 0)));
-
-        WaterAlarmOutDTO row = ledgerPage().getRecords().get(0);
-
-        assertEquals(2, row.getDisplayStatus());
-        assertEquals("已转工单", row.getDisplayStatusText());
-        assertEquals("WO202606101703482640", row.getOrderNo());
-    }
-
-    @Test
-    void falseAlarmAuditWithoutOrderDoesNotImplyRecovery() {
-        AlarmData falseAlarm = alarm("alarm-1", "CN-GS-202609020002", "流量预警", 0, 2);
-        falseAlarm.setAuditStatus(com.zksy.base.alarm.enums.AlarmAuditStatusEnum.FALSE_ALARM.getCode());
-        stubLedger(falseAlarm);
-        when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
-                status("EQ-F-1", 0, 1, 1)));
-
-        WaterAlarmOutDTO row = ledgerPage().getRecords().get(0);
-
-        assertEquals(4, row.getDisplayStatus());
-        assertEquals("误报警", row.getDisplayStatusText());
-        // 报警记录自身仍未处理,但设备已不在报警中:展示状态与设备报警状态各自独立
-        assertEquals(0, row.getDeviceAlarmStatus());
-        assertEquals("正常", row.getDeviceAlarmStatusText());
-    }
-
-    @Test
-    void ledgerQueryIsScopedToWaterDevices() {
-        stubLedger();
-        when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.emptyList());
-
-        ledgerPage();
-
-        ArgumentCaptor<AbstractWrapper<AlarmData, ?, ?>> captor = ArgumentCaptor.forClass(AbstractWrapper.class);
-        verify(alarmDataService).list(captor.capture());
-        String sql = captor.getValue().getSqlSegment();
-        List<Object> values = Arrays.asList(captor.getValue().getParamNameValuePairs().values().toArray());
-        // 只允许供水设备编码进入 IN 条件,不得混入排水/燃气/窨井设备
-        assertEquals(true, sql.contains("device_code"));
-        assertEquals(true, values.contains("CN-GS-202609020001"));
-        assertEquals(true, values.contains("CN-GS-202609020002"));
-        assertEquals(false, values.contains("YJ-EE-20260702"));
-    }
-
-    private static EquipmentStatus status(String equipmentId, Integer alarmStatus, Integer currentStatus,
-                                          Integer onlineStatus) {
-        EquipmentStatus status = new EquipmentStatus();
-        status.setEquipmentId(equipmentId);
-        status.setAlarmStatus(alarmStatus);
-        status.setCurrentStatus(currentStatus);
-        status.setOnlineStatus(onlineStatus);
-        return status;
-    }
-}

+ 0 - 2
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmRealtimeContractTest.java

@@ -78,8 +78,6 @@ class WaterSupplyAlarmRealtimeContractTest {
                 new WaterSupplyQueryService(equipmentBaseService, equipmentBaseMapper,
                         equipmentStatusMapper, equipmentTypeMapper, workOrderMapper,
                         alarmDataMapper));
-        ReflectionTestUtils.setField(service, "equipmentBaseMapper", equipmentBaseMapper);
-        ReflectionTestUtils.setField(service, "workOrderMapper", workOrderMapper);
 
         EquipmentBase device = device("EQ-P-1", "CN-GS-202609020001", "城南压力监测点");
         when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Collections.singletonList(device));

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

@@ -30,6 +30,15 @@ class AlarmOperationsMapperTest {
             assertFalse(sql.contains("a.actual_value IS NOT NULL"));
             assertTrue(sql.contains("w.order_id = a.work_order_id")); rendered.put(name,sql);
         }
+        query.setKeyword("设备搜索");
+        for (String name : Arrays.asList("selectPage", "selectReminders", "countByLevel", "selectMap")) {
+            String keywordSql = sql(config,name,params);
+            assertTrue(keywordSql.contains("a.device_code ILIKE CONCAT('%',?,'%')"));
+            assertTrue(keywordSql.contains("OR e.equipment_name ILIKE CONCAT('%',?,'%')"));
+            assertFalse(keywordSql.contains("e.equipment_location ILIKE"));
+            assertTrue(keywordSql.contains("e.equipment_type_id IN"));
+        }
+        query.setKeyword(null);
         query.setActualValueOnly(true);
         String numeric = sql(config,"selectPage",params);
         assertTrue(numeric.contains("a.actual_value IS NOT NULL"));

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

@@ -73,9 +73,9 @@ class AlarmOperationsServiceTest {
         assertEquals(Collections.emptyList(),service.logs("a1",query));
         verify(logs).selectHistory("a1");
     }
-    @Test void invalidFiltersAndNonDrainageAnalysisAreRejected() {
+    @Test void invalidFiltersAndNonWaterSubsystemAnalysisAreRejected() {
         query.setAlarmStatus(5); assertThrows(ServiceException.class, () -> service.page(query));
-        query.setRootTypeId(1); assertThrows(ServiceException.class, () -> service.map(query));
+        query.setRootTypeId(3); assertThrows(ServiceException.class, () -> service.map(query));
         assertThrows(ServiceException.class, () -> service.analysis("a1",query,AlarmSeriesPeriod.CURVE));
         verifyNoInteractions(mapper);
     }
@@ -104,10 +104,51 @@ class AlarmOperationsServiceTest {
         assertEquals(true,result.get("supported"));
         verify(mapper).selectSamples(AlarmMetricSource.INSTANT_FLOW,"D1",LocalDateTime.of(2026,1,10,0,0),LocalDateTime.of(2026,1,11,0,0),"DAY");
     }
+    @Test void mapForcesUnhandledStatusWithOrWithoutRequestedStatus() {
+        when(mapper.selectMap(eq(query),anyList())).thenAnswer(call -> {
+            AlarmQuery filter = call.getArgument(0);
+            assertEquals(AlarmRecordStatusEnum.UNHANDLED.getCode(), filter.getAlarmStatus());
+            assertEquals(Arrays.asList(2,21), call.getArgument(1));
+            return Collections.emptyList();
+        });
+        for (Integer status : Arrays.asList(null, 0, 1)) {
+            query.setAlarmStatus(status);
+            assertTrue(service.map(query).isEmpty());
+        }
+        verify(mapper,times(3)).selectMap(eq(query),anyList());
+    }
     @Test void mapNeverSilentlyTruncatesLargeResult() {
         when(mapper.selectMap(eq(query),anyList())).thenReturn(Collections.nCopies(10001,alarm));
         assertThrows(ServiceException.class, () -> service.map(query));
     }
+    @Test void waterSupplyMapUsesAuthorizedTypeTreeAndForcesCurrentAlarms() {
+        query.setRootTypeId(1); query.setAlarmStatus(AlarmRecordStatusEnum.HANDLED.getCode());
+        List<Integer> ids = Arrays.asList(1,11,111);
+        when(scope.resolveOperations(query)).thenReturn(ids);
+        when(mapper.selectMap(query,ids)).thenReturn(Collections.singletonList(alarm));
+        assertEquals(Collections.singletonList(alarm),service.map(query));
+        assertEquals(AlarmRecordStatusEnum.UNHANDLED.getCode(),query.getAlarmStatus());
+        verify(mapper).selectMap(query,ids);
+    }
+    @Test void waterSupplyPressureCurveUsesRealSamplesWithinAuthorizedScope() {
+        query.setRootTypeId(1); alarm.setWarningCode("WARN-PRESSURE");
+        List<Integer> ids = Arrays.asList(1,11);
+        when(scope.resolveOperations(query)).thenReturn(ids);
+        when(mapper.selectSamples(eq(AlarmMetricSource.PRESSURE),eq("D1"),any(),any(),eq("CURVE")))
+                .thenReturn(Collections.emptyList());
+        Map<String,Object> result = service.analysis("a1",query,AlarmSeriesPeriod.CURVE);
+        assertEquals(true,result.get("supported"));
+        verify(mapper).selectDetail("a1",query,ids);
+        verify(mapper).selectSamples(AlarmMetricSource.PRESSURE,"D1",
+                LocalDateTime.of(2026,1,10,11,0),LocalDateTime.of(2026,1,10,13,0),"CURVE");
+    }
+    @Test void unauthorizedWaterSupplyScopeCannotReadMapOrCurveSamples() {
+        query.setRootTypeId(1);
+        when(scope.resolveOperations(query)).thenThrow(new org.springframework.security.access.AccessDeniedException("denied"));
+        assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> service.map(query));
+        assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> service.analysis("a1",query,AlarmSeriesPeriod.CURVE));
+        verifyNoInteractions(mapper);
+    }
     @Test void dashboardKeepsTotalsAndGroupsFourLevelsOnConsecutiveBeijingDates() {
         query.setRootTypeId(1);
         List<Integer> ids = Arrays.asList(1, 11);

+ 14 - 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/index"); menu.setStatus("0");
+        SysMenu menu = new SysMenu(); menu.setComponent("subSystem/alarm/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");
@@ -48,7 +48,7 @@ class AlarmReviewScopeServiceTest {
         assertThrows(AccessDeniedException.class, () -> service.resolveOperations(query));
     }
     @Test void gisComponentUsesUpdatedNameAndDisabledMenuIsDenied() {
-        SysMenu menu = new SysMenu(); menu.setStatus("0"); menu.setComponent("subSystem/alarm/drainage/alarmGIS");
+        SysMenu menu = new SysMenu(); menu.setStatus("0"); menu.setComponent("subSystem/drainage/alarm/alarmGIS");
         menu.setQuery("{\"rootTypeId\":2}"); when(menus.selectMenuList(9L)).thenReturn(Collections.singletonList(menu));
         when(types.selectList(null)).thenReturn(Arrays.asList(type(2,0),type(21,2),type(22,21),type(12,1)));
         query.setRootTypeId(2); query.setWarningCode(null);
@@ -58,4 +58,16 @@ class AlarmReviewScopeServiceTest {
     @Test void reviewMenuDoesNotGrantOperationsAccess() {
         assertThrows(AccessDeniedException.class, () -> service.resolveOperations(query));
     }
+    @Test void waterSupplyGisMenuOnlyGrantsWaterOperationsAndNotAudit() {
+        SysMenu menu = new SysMenu(); menu.setStatus("0"); menu.setComponent("subSystem/waterSupply/alarm/alarmGIS");
+        menu.setQuery("{\"rootTypeId\":1}"); when(menus.selectMenuList(9L)).thenReturn(Collections.singletonList(menu));
+        when(types.selectList(null)).thenReturn(Arrays.asList(type(1,0),type(11,1),type(111,11),type(2,0),type(21,2)));
+        query.setRootTypeId(1); query.setWarningCode(null);
+        assertEquals(Arrays.asList(1,11,111),service.resolveOperations(query));
+        assertThrows(AccessDeniedException.class, () -> service.resolve(query));
+        query.setRootTypeId(2);
+        assertThrows(AccessDeniedException.class, () -> service.resolveOperations(query));
+        query.setRootTypeId(1); menu.setStatus("1");
+        assertThrows(AccessDeniedException.class, () -> service.resolveOperations(query));
+    }
 }