Kaynağa Gözat

报警审核整合

zengy 3 hafta önce
ebeveyn
işleme
ac65ff69b6
57 değiştirilmiş dosya ile 1272 ekleme ve 2029 silme
  1. 46 0
      docs/alarm-review-2026-09-23.md
  2. 2 2
      pipe-network-service/CONTEXT.md
  3. 111 0
      pipe-network-service/sql/migration_alarm_review.sql
  4. 79 0
      pipe-network-service/sql/tests/alarm_review.test.mjs
  5. 0 75
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmController.java
  6. 74 43
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java
  7. 0 35
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasMonitorController.java
  8. 0 70
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmRetiredEndpointTest.java
  9. 42 0
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/alarm/AlarmReviewControllerTest.java
  10. 0 121
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/domain/AlarmAudit.java
  11. 0 44
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/AlarmAuditInDTO.java
  12. 0 48
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/AlarmDispatchInDTO.java
  13. 0 35
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmAuditOrderStatVO.java
  14. 0 53
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmAuditOutDTO.java
  15. 0 32
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmFlowOutDTO.java
  16. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/WaterAlarmOutDTO.java
  17. 0 13
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/mapper/AlarmAuditMapper.java
  18. 0 37
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/IWaterSupplyAlarmService.java
  19. 26 475
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java
  20. 2 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/AlarmData.java
  21. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/AlarmLog.java
  22. 31 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmReviewQuery.java
  23. 16 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmReviewRequest.java
  24. 15 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmReviewVO.java
  25. 19 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmAuditStatusEnum.java
  26. 17 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmDisplayStatusEnum.java
  27. 19 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmLevelEnum.java
  28. 19 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmLogOperTypeEnum.java
  29. 19 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmRecordStatusEnum.java
  30. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/mapper/AlarmLogMapper.java
  31. 18 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/mapper/AlarmReviewMapper.java
  32. 57 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewAttachmentService.java
  33. 58 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewScopeService.java
  34. 135 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewService.java
  35. 4 33
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmClearServiceImpl.java
  36. 7 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmDataServiceImpl.java
  37. 9 62
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillAlarmSyncService.java
  38. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/impl/DrillSimulationServiceImpl.java
  39. 8 60
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/DrillAlarmSyncService.java
  40. 0 16
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/GasMonitorService.java
  41. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/DrillSimulationServiceImpl.java
  42. 0 233
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/GasMonitorServiceImpl.java
  43. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/service/WorkOrderService.java
  44. 21 28
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/service/impl/WorkOrderServiceImpl.java
  45. 57 0
      pipe-network-service/zksy-system/src/main/resources/mapper/base/AlarmReviewMapper.xml
  46. 0 391
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmAuditServiceTest.java
  47. 2 13
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmDashboardServiceTest.java
  48. 6 18
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmLedgerServiceTest.java
  49. 0 10
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmRealtimeContractTest.java
  50. 1 17
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmClearServiceTest.java
  51. 31 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmReviewAttachmentServiceTest.java
  52. 47 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmReviewMapperTest.java
  53. 41 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmReviewScopeServiceTest.java
  54. 118 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmReviewServiceTest.java
  55. 58 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/ops/service/impl/WorkOrderServiceImplTest.java
  56. 4 0
      zk-api-service/src/main/java/com/zksy/api/domain/AlarmData.java
  57. 6 56
      zk-api-service/src/main/java/com/zksy/api/service/impl/AlarmDataServiceImpl.java

+ 46 - 0
docs/alarm-review-2026-09-23.md

@@ -0,0 +1,46 @@
+# 统一报警审核交付说明
+
+## 已确认契约
+
+- 前端 `city-life-line/src/views/subSystem/alarm/index.vue`,后端 `AlarmController`,独立的审核服务位于 `base/alarm`。
+- 仅 `alarm_status=0` 进入审核列表,一条事件一行,不按设备去重;后端分页和时间排序。
+- `audit_status`:0 未审核、1 确认报警、2 误报警。`alarm_level` 继续使用 1~4(一级最严重,红/橙/蓝/绿),不新增或更名为 `case_level`。
+- 确认报警原子更新审核状态、记录审核日志并创建工单;任一步失败整个审核事务回滚。不修改设备状态,也不将报警标为已处理。误报警仅记录结论及意见。
+- 审核转工单时,`work_order.order_desc` 根据原报警信息生成故障描述,审核意见单独记录到 `alarm_log.oper_desc`,不再覆盖故障描述。实际值为空的报警,页面及自动生成的工单描述均不显示实际值/阈值占位;数值 0 仍视为有效值。
+- 报警生成不再提前创建工单;保留井盖必要建案,确认建单时回填已有案件的 `order_id`。其他工单入口及既有同设备未办结工单冲突校验保留,发生冲突会返回明确错误并回滚审核。
+- 意见必填、最多 500 字;只允许 JPG/JPEG/PNG 图片,最多 5 张、单张 10 MB,前后端均拒绝 PDF。处置记录显示图片缩略图,点击可查看原图并切换同条记录的多张图片。图片复用管理端框架上传目录,票据在 Redis 保留两小时,绑定当前用户和报警。
+
+## 脚本
+
+`pipe-network-service/sql/migration_alarm_review.sql` 按 KingbaseES PostgreSQL 兼容语法编写。
+
+- 只做结构和菜单调整,不复制旧 `alarm_audit`、不补历史日志、不推测历史审核结果;旧表数据不删除,但新代码不再读写。
+- `alarm_log.log_id int8 DEFAULT nextval('app_user.alarm_log_id_seq'::regclass)`;序列单独定义并 `OWNED BY`,Java 为 `Long` + `IdType.AUTO`。不新增 `source_key`。
+- 通过事务内触发器覆盖各服务的报警 INSERT,初始化日志;报警处理/工单关联、工单操作日志同步进入报警时间线。
+- 精准更新已有菜单 2118/2142/2097/2024,`path='alarmAudit'`、`component='subSystem/alarm/index'`。设备类型根分别为 1/2/3/4,燃气额外 `warning_code='WARN-GAS-CONCENTRATION'`;保留原父级和角色关联。
+- 页面权限为 `alarm:review:list`;仅在缺失时创建 `alarm:review:audit` 按钮权限,给对应页面的现有角色补充新按钮授权,重复执行不恢复已经撤销的按钮授权。不会重复新增四个页面菜单。
+- `rootTypeId` 代表 `equipment_type.id` 的根,查询包含所有后代;服务端校验当前用户实际获授权的菜单范围,不能仅修改前端 query 跨系统访问。
+
+上线前备份、停止报警/工单写入,完整执行脚本,再部署管理端、采集 API 和前端并重新登录刷新菜单。脚本依赖现有 `app_user` schema、报警/工单日志/菜单表;既有列若来自其他不兼容的试验版本,不做数据修复或强制转换。
+
+## REST 审核接口
+
+公共前缀 `/api/alarm`。列表、详情、日志、上传、提交均携带 `rootTypeId`,必要时带 `warningCode`。
+
+- `GET /reviews`:pageNum/pageSize、keyword、alarmLevel、auditStatus、startTime/endTime、ascending。
+- `GET /reviews/statistics`:同设备/等级/日期条件,返回 total/pending。
+- `GET /reviews/options`:等级、审核结果和根类型枚举。
+- `GET /reviews/{alarmId}`:详情。
+- `GET /{alarmId}/logs`:时间线。
+- `POST /{alarmId}/attachments`:Multipart 文件,返回附件票据。
+- `POST /{alarmId}/reviews`:auditStatus、opinion、attachmentTokens。
+
+其他仍被报警总览、地图和独立页面使用的通用接口保留。供水旧审核/停用写接口、通用 `approve-to-work-order`、旧燃气浓度监测页面独占接口与五个旧 Vue 入口已移除,可通过 Git 恢复源码。
+
+## 验证与未完成验收
+
+- 管理端 Maven clean test:审核 service/scope/mapper/附件/controller、供水台账/统计/实时/GIS相关契约、清警、工单共 55 项通过。
+- 前端生产构建通过,保留原有 Sass 弃用/包体积提示;相关 Node 回归 14 项通过。
+- 内存 PGlite SQL 回归 7 项通过:显式序列、精确菜单、初始化/关联/处理日志、回滚、重跑及权限撤销保留。测试文件 `pipe-network-service/sql/tests/alarm_review.test.mjs`。
+- 未连接或修改实际数据库;PGlite 不是 KingbaseES 实机验收,也未执行真实登录端到端测试。
+- `zk-api-service` 单独编译被既有缺失类 `com.zksy.api.service.CollectionAlarmWriter` 阻塞。HEAD 已引用该类,但 Git 树中无对应文件;本轮不臆造采集恢复实现。部署采集 API 前需要补回该类并重新编译、联调。

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

@@ -53,7 +53,7 @@ _Avoid_:把供水子系统等同于单一的报警页面,或把监测设备
 
 4.1 **报警管理**:依据各类监测设备的阈值规则和其他自动报警规则触发报警,展示报警等级、设备报警状态、报警展示状态及关联工单;本页面为报警台账总览,支持态势统计、趋势分析、分类占比、详情查看、批量导出和跳转报警审核,不承载审核、转工单、解除或派发操作。不同报警类型可形成不同工单;同一设备同一报警类型同时只能存在一个未结束的同类报警工单。
 
-4.2 **报警审核**:作为报警审核处置工作台,仅提供“转工单”和“误报解除”两个审核结果,并跟踪关联工单进度;完整派单、接单、处理、验收等工单处置动作由统一工单页承载。报警审核是供水报警人工处置的唯一写入口:旧的“一键标记已处理”与“直接派发工单”两个接口已停用(#46 收口裁定,实现与路由暂时保留,去留由后续工单决定),调用一律返回停用提示且不修改任何数据。运行采集系统在收到有效恢复数据时,可以自动处理同设备、同报警编码的未处理告警;数值回到预警区即可处理,处理记录注明“系统自动”,不自动完成关联工单(2026-09-23用户确认,见仓库docs/collection-alarm-recovery-2026-09-23.md)。
+4.2 **报警审核**:四个子系统统一使用 `AlarmController` 的审核接口和 `subSystem/alarm/index` 页面,列表仅展示 `alarm_status=0` 的事件。`audit_status` 独立记录未审核、确认报警、误报警;确认报警时创建待派单工单,误报警只记录结论,两种审核都不直接修改设备状态或报警处理状态。完整派单、接单、处理、验收由统一工单页承载。旧供水审核、直接派单和停用的解除接口已删除。报警生成、审核、处理及关联工单操作记录在 `alarm_log`,不回填历史数据。运行采集系统在收到有效恢复数据时,可以自动处理同设备、同报警编码的未处理告警;数值回到预警区即可处理,处理记录注明“系统自动”,不自动完成关联工单(2026-09-23用户确认,见仓库docs/collection-alarm-recovery-2026-09-23.md)。
 
 4.3 **阈值管理**:支持按传感器分类批量设置阈值、设置分时阈值并查询已配置点位的阈值详情。
 
@@ -155,7 +155,7 @@ _Avoid_:报警记录、未结工单
 _Avoid_:设备报警状态、设备异常
 
 **报警展示状态**:
-由报警持久化状态、审核结果和关联工单推导出的面向用户的状态,用于区分待审核、已转工单和已解除;它不替代设备报警状态。
+由报警持久化状态、审核结果和关联工单推导出的面向用户的状态,用于区分待审核、已转工单、已解除和误报警;它不替代设备报警状态,误报警不代表监测数据已恢复。
 _Avoid_:报警记录持久化状态、设备报警状态
 
 **报警记录状态**:

+ 111 - 0
pipe-network-service/sql/migration_alarm_review.sql

@@ -0,0 +1,111 @@
+-- 统一报警审核。KingbaseES(PostgreSQL 兼容模式)。先备份,停止报警/工单写入,
+-- 完整执行事务,再同时部署管理端、API 服务和前端;不要拆开执行或忽略错误。
+-- audit_status 与 alarm_status 独立;确认报警创建工单,但不改变设备或报警处理状态。
+-- 日志操作字典与 AlarmLogOperTypeEnum 对齐:0触发/1确认/2误报/3处理/4关联工单/5工单操作。
+-- 用数据库触发器统一覆盖各采集服务、API、演练及直接入库路径,不在多个服务重复初始化日志。
+-- 仅调整结构与菜单,不迁移或补录历史数据;旧 alarm_audit 表不再由新代码读写。
+BEGIN;
+LOCK TABLE app_user.alarm_data, app_user.work_order_log IN SHARE ROW EXCLUSIVE MODE;
+LOCK TABLE app_user.sys_menu, app_user.sys_role_menu IN SHARE ROW EXCLUSIVE MODE;
+
+ALTER TABLE app_user.alarm_data ADD COLUMN IF NOT EXISTS audit_status integer DEFAULT 0;
+ALTER TABLE app_user.alarm_data ALTER COLUMN audit_status SET DEFAULT 0;
+ALTER TABLE app_user.alarm_data ALTER COLUMN audit_status SET NOT NULL;
+COMMENT ON COLUMN app_user.alarm_data.audit_status IS '审核结果:0-未审核 1-确认报警 2-误报警,独立于报警处理状态';
+DO $$ BEGIN
+    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'app_user.alarm_data'::regclass
+                   AND conname = 'ck_alarm_data_audit_status') THEN
+        ALTER TABLE app_user.alarm_data ADD CONSTRAINT ck_alarm_data_audit_status CHECK (audit_status IN (0,1,2));
+    END IF;
+END $$;
+
+-- KingbaseES 显式序列自增,与 work_order_log 的主键策略一致,各表使用独立序列。
+CREATE SEQUENCE IF NOT EXISTS app_user.alarm_log_id_seq
+    START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
+CREATE TABLE IF NOT EXISTS app_user.alarm_log (
+    log_id int8 NOT NULL DEFAULT nextval('app_user.alarm_log_id_seq'::regclass) PRIMARY KEY,
+    alarm_id varchar(64) NOT NULL,
+    oper_type integer NOT NULL CHECK (oper_type IN (0,1,2,3,4,5)),
+    oper_desc text,
+    attach_url text,
+    oper_user_id int8,
+    oper_user_name varchar(128),
+    oper_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+ALTER SEQUENCE app_user.alarm_log_id_seq OWNED BY app_user.alarm_log.log_id;
+COMMENT ON COLUMN app_user.alarm_log.log_id IS '日志主键,int8,使用 alarm_log_id_seq 序列自增长';
+COMMENT ON TABLE app_user.alarm_log IS '报警操作日志,保留报警触发、审核、处理及关联工单的全流程';
+COMMENT ON COLUMN app_user.alarm_log.alarm_id IS '关联 alarm_data.id';
+COMMENT ON COLUMN app_user.alarm_log.oper_type IS 'AlarmLogOperTypeEnum:0触发 1确认 2误报 3处理 4关联工单 5工单操作';
+COMMENT ON COLUMN app_user.alarm_log.oper_desc IS '操作详情或审核意见';
+COMMENT ON COLUMN app_user.alarm_log.attach_url IS '附件地址,多个用逗号分隔';
+COMMENT ON COLUMN app_user.alarm_log.oper_user_id IS '操作人ID,系统操作可为空';
+COMMENT ON COLUMN app_user.alarm_log.oper_user_name IS '操作人名称';
+COMMENT ON COLUMN app_user.alarm_log.oper_time IS '操作时间';
+CREATE INDEX IF NOT EXISTS idx_alarm_log_alarm_time ON app_user.alarm_log(alarm_id, oper_time, log_id);
+CREATE INDEX IF NOT EXISTS idx_alarm_review_query ON app_user.alarm_data(alarm_status, audit_status, alarm_time DESC);
+
+CREATE OR REPLACE FUNCTION app_user.record_alarm_lifecycle() RETURNS trigger AS $$
+BEGIN
+    IF TG_OP = 'INSERT' THEN
+        INSERT INTO app_user.alarm_log(alarm_id, oper_type, oper_desc, oper_user_name, oper_time)
+        VALUES (NEW.id, 0, concat(coalesce(NEW.warning_type, NEW.warning_code, '监测报警'), ';实际值:', NEW.actual_value,
+                ';触发时下限:', NEW.min_value, ';触发时上限:', NEW.max_value),
+                '监测系统', coalesce(NEW.alarm_time, NEW.create_time, CURRENT_TIMESTAMP));
+    ELSIF NEW.alarm_status IS DISTINCT FROM OLD.alarm_status AND NEW.alarm_status = 1 THEN
+        INSERT INTO app_user.alarm_log(alarm_id, oper_type, oper_desc, oper_user_name, oper_time)
+        VALUES (NEW.id, 3, coalesce(NEW.handle_remark, '报警已处理'), coalesce(NEW.handle_user, '监测系统'),
+                coalesce(NEW.handle_time, clock_timestamp()));
+    END IF;
+    IF (TG_OP = 'INSERT' AND NEW.order_id IS NOT NULL)
+       OR (TG_OP = 'UPDATE' AND NEW.order_id IS DISTINCT FROM OLD.order_id AND NEW.order_id IS NOT NULL) THEN
+        INSERT INTO app_user.alarm_log(alarm_id, oper_type, oper_desc, oper_user_name, oper_time)
+        VALUES (NEW.id, 4, '已关联工单ID:' || NEW.order_id, '系统', clock_timestamp());
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS trg_alarm_lifecycle_log ON app_user.alarm_data;
+CREATE TRIGGER trg_alarm_lifecycle_log AFTER INSERT OR UPDATE OF alarm_status, order_id ON app_user.alarm_data
+FOR EACH ROW EXECUTE PROCEDURE app_user.record_alarm_lifecycle();
+
+CREATE OR REPLACE FUNCTION app_user.record_alarm_work_order_log() RETURNS trigger AS $$
+BEGIN
+    INSERT INTO app_user.alarm_log(alarm_id, oper_type, oper_desc, attach_url, oper_user_id, oper_user_name, oper_time)
+    SELECT a.id, 5, NEW.oper_desc, NEW.attach_url, NEW.oper_user_id, NEW.oper_user_name,
+           coalesce(NEW.oper_time, CURRENT_TIMESTAMP)
+    FROM app_user.alarm_data a WHERE a.order_id = NEW.order_id;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS trg_work_order_alarm_log ON app_user.work_order_log;
+CREATE TRIGGER trg_work_order_alarm_log AFTER INSERT ON app_user.work_order_log
+FOR EACH ROW EXECUTE PROCEDURE 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',
+    query = CASE menu_id
+        WHEN 2118 THEN '{"rootTypeId":1}'
+        WHEN 2142 THEN '{"rootTypeId":2}'
+        WHEN 2097 THEN '{"rootTypeId":3,"warning_code":"WARN-GAS-CONCENTRATION"}'
+        WHEN 2024 THEN '{"rootTypeId":4}' END,
+    update_time = CURRENT_TIMESTAMP
+WHERE menu_id IN (2118,2142,2097,2024);
+
+-- 仅为新建按钮补角色授权,重复执行不得恢复管理员已经撤销的审核权限。
+WITH new_buttons AS (
+INSERT INTO app_user.sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache,
+                              menu_type, visible, status, perms, icon, create_by, create_time)
+SELECT (SELECT coalesce(max(menu_id), 0) FROM app_user.sys_menu) + row_number() OVER (ORDER BY p.menu_id),
+       '报警审核', p.menu_id, 1, '', NULL, 1, 0, 'F', '0', '0', 'alarm:review:audit', '#', 'admin', CURRENT_TIMESTAMP
+FROM app_user.sys_menu p WHERE p.menu_id IN (2118,2142,2097,2024)
+AND NOT EXISTS (SELECT 1 FROM app_user.sys_menu c WHERE c.parent_id = p.menu_id AND c.perms = 'alarm:review:audit')
+RETURNING menu_id, parent_id
+)
+INSERT INTO app_user.sys_role_menu(role_id, menu_id)
+SELECT DISTINCT rm.role_id, c.menu_id FROM app_user.sys_role_menu rm
+JOIN new_buttons c ON c.parent_id = rm.menu_id
+WHERE rm.menu_id IN (2118,2142,2097,2024)
+AND NOT EXISTS (SELECT 1 FROM app_user.sys_role_menu existing WHERE existing.role_id = rm.role_id AND existing.menu_id = c.menu_id);
+
+COMMIT;

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

@@ -0,0 +1,79 @@
+// PostgreSQL 兼容性回归,不替代 KingbaseES 最终验收;不连接实际数据库。
+// node --test alarm_review.test.mjs;通过 PGLITE_MODULE 指向已有 PGlite ESM 模块。
+import { readFile } from 'node:fs/promises'
+import { pathToFileURL } from 'node:url'
+import assert from 'node:assert/strict'
+import { test, after } from 'node:test'
+
+const { PGlite } = await import(process.env.PGLITE_MODULE ? pathToFileURL(process.env.PGLITE_MODULE).href : '@electric-sql/pglite')
+const db = new PGlite()
+const sql = await readFile(new URL('../migration_alarm_review.sql', import.meta.url), 'utf8')
+await db.exec(`
+ CREATE SCHEMA app_user;
+ CREATE TABLE app_user.alarm_data(id varchar(64) PRIMARY KEY, alarm_status integer DEFAULT 0, alarm_level integer,
+ warning_code text, warning_type text, actual_value numeric, min_value numeric, max_value numeric,
+ alarm_time timestamp, create_time timestamp, handle_remark text, handle_user text, handle_time timestamp, order_id int8);
+ CREATE TABLE app_user.work_order_log(log_id int8, order_id int8, oper_desc text, attach_url text,
+ oper_user_id int8, oper_user_name text, oper_time timestamp);
+ CREATE TABLE app_user.sys_menu(menu_id int8 PRIMARY KEY, menu_name text, parent_id int8, order_num integer,
+ path text, component text, query text, is_frame integer, is_cache integer, menu_type text, visible text,
+ status text, perms text, icon text, create_by text, create_time timestamp, update_time timestamp);
+ CREATE TABLE app_user.sys_role_menu(role_id int8, menu_id int8, PRIMARY KEY(role_id, menu_id));
+ INSERT INTO app_user.alarm_data(id,alarm_level) VALUES ('history',1);
+ INSERT INTO app_user.sys_menu(menu_id,parent_id,path,component) VALUES
+ (2118,100,'audit','water'),(2142,101,'bjsh','drainage'),(2097,102,'krqildjc','gas'),(2024,103,'alarmList','manhole'),(3000,104,'keep','keep');
+ INSERT INTO app_user.sys_role_menu VALUES (9,2118),(9,2142),(9,2097),(9,2024);
+`)
+await db.exec(sql)
+const rows = async query => (await db.query(query)).rows
+after(() => db.close())
+
+test('int8 primary key uses dedicated owned sequence, no auxiliary source key or history backfill', async () => {
+  const [column] = await rows("SELECT udt_name,column_default FROM information_schema.columns WHERE table_schema='app_user' AND table_name='alarm_log' AND column_name='log_id'")
+  assert.equal(column.udt_name, 'int8'); assert.match(column.column_default, /nextval\('app_user.alarm_log_id_seq'::regclass\)/)
+  assert.equal((await rows("SELECT pg_get_serial_sequence('app_user.alarm_log','log_id') AS seq"))[0].seq, 'app_user.alarm_log_id_seq')
+  assert.equal((await rows('SELECT * FROM app_user.alarm_log')).length, 0)
+  assert.equal((await rows("SELECT column_name FROM information_schema.columns WHERE table_schema='app_user' AND column_name='source_key'")).length, 0)
+  assert.equal((await rows("SELECT audit_status,alarm_level FROM app_user.alarm_data WHERE id='history'"))[0].alarm_level, 1)
+})
+test('four existing menu IDs get alarmAudit and exact independent scopes, unrelated menu untouched', async () => {
+  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.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')
+  assert.equal((await rows("SELECT * FROM app_user.sys_menu WHERE perms='alarm:review:audit'")).length,4)
+})
+test('new alarm initializes one snapshot log with generated ID', async () => {
+  await db.exec("INSERT INTO app_user.alarm_data(id,warning_type,actual_value,min_value,max_value) VALUES ('a1','压力',0,1,10)")
+  const logs = await rows("SELECT * FROM app_user.alarm_log WHERE alarm_id='a1'")
+  assert.equal(logs.length,1); assert.equal(logs[0].oper_type,0); assert.ok(logs[0].log_id > 0)
+  assert.match(logs[0].oper_desc,/实际值:0/)
+})
+test('order relation and work order operations are logged, unrelated updates do not duplicate logs', async () => {
+  await db.exec("UPDATE app_user.alarm_data SET order_id=88 WHERE id='a1'; UPDATE app_user.alarm_data SET order_id=88 WHERE id='a1'; INSERT INTO app_user.work_order_log(order_id,oper_desc) VALUES (88,'创建工单');")
+  const logs = await rows("SELECT oper_type FROM app_user.alarm_log WHERE alarm_id='a1' ORDER BY log_id")
+  assert.deepEqual(logs.map(x=>x.oper_type),[0,4,5])
+})
+test('handling log records recovery but does not modify audit result', async () => {
+  await db.exec("UPDATE app_user.alarm_data SET audit_status=1,alarm_status=1,handle_remark='监测恢复' WHERE id='a1'")
+  assert.equal((await rows("SELECT audit_status FROM app_user.alarm_data WHERE id='a1'"))[0].audit_status,1)
+  assert.equal((await rows("SELECT oper_desc FROM app_user.alarm_log WHERE alarm_id='a1' AND oper_type=3"))[0].oper_desc,'监测恢复')
+})
+test('repeated deployment preserves existing logs, IDs and intentional permission revocations', async () => {
+  const oldLogs = await rows('SELECT log_id FROM app_user.alarm_log ORDER BY log_id')
+  await db.exec("DELETE FROM app_user.sys_role_menu WHERE menu_id IN (SELECT menu_id FROM app_user.sys_menu WHERE parent_id=2118 AND perms='alarm:review:audit')")
+  await db.exec(sql)
+  assert.deepEqual(await rows('SELECT log_id FROM app_user.alarm_log ORDER BY log_id'), oldLogs)
+  assert.equal((await rows("SELECT * FROM app_user.sys_menu WHERE perms='alarm:review:audit'")).length,4)
+  assert.equal((await rows("SELECT * FROM app_user.sys_role_menu WHERE menu_id IN (SELECT menu_id FROM app_user.sys_menu WHERE parent_id=2118 AND perms='alarm:review:audit')")).length,0)
+  await db.exec("INSERT INTO app_user.alarm_data(id) VALUES ('a2')")
+  assert.ok((await rows("SELECT log_id FROM app_user.alarm_log WHERE alarm_id='a2'"))[0].log_id > oldLogs.at(-1).log_id)
+})
+test('transaction rollback also rolls back initialized logs', async () => {
+  await db.exec("BEGIN; INSERT INTO app_user.alarm_data(id) VALUES ('rolled-back'); ROLLBACK;")
+  assert.equal((await rows("SELECT * FROM app_user.alarm_log WHERE alarm_id='rolled-back'")).length,0)
+  await assert.rejects(db.exec("UPDATE app_user.alarm_data SET audit_status=3 WHERE id='a2'"))
+})

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

@@ -1,11 +1,7 @@
 package com.zksy.web.controller.WaterSupply.MonitorAlarm;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmAuditInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmDispatchInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmDashboardOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmFlowOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
@@ -62,10 +58,6 @@ public class WaterSupplyAlarmController {
         return AjaxResult.success(alarmClearService.reviewAllWaterDevices("手工复核"));
     }
 
-    /** 已停用写接口的统一提示:供水报警的写入口只有报警审核(转工单 / 误报解除)。 */
-    private static final String RETIRED_WRITE_ENDPOINT_MESSAGE =
-            "该接口已停用:供水报警的写入口统一为报警审核(转工单 / 误报解除),请改用 /waterSupply/alarm/audit";
-
     @GetMapping("/stats")
     @ApiOperation("监测报警统计(实时报警数/今日报警总数/设备异常率/平均响应时间)")
     @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
@@ -170,73 +162,6 @@ public class WaterSupplyAlarmController {
         new ExcelUtil<>(WaterAlarmLedgerExportDTO.class).exportExcel(response, rows, "供水报警台账");
     }
 
-    /**
-     * 【已停用】旧的「一键标记已处理」写入口。
-     *
-     * <p>#46 收口裁定:供水报警的写入口收敛到报警审核(转工单 / 误报解除),
-     * 报警管理与设备异常管理只读,本接口不再修改任何数据。方法签名与路由暂时保留,
-     * 去留由后续工单决定(可能与运行系统清警能力或跨系统对接一并处理)。</p>
-     */
-    @Deprecated
-    @PutMapping("/resolve/{alarmId}")
-    @ApiOperation("【已停用】一键标记已处理")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:handle')")
-    public AjaxResult resolveAlarm(@PathVariable String alarmId,
-                                   @ApiParam(value = "处理备注(接口已停用,入参不再生效)")
-                                   @RequestParam(required = false) String handleRemark) {
-        return AjaxResult.error(RETIRED_WRITE_ENDPOINT_MESSAGE);
-    }
-
-    /**
-     * 【已停用】旧的「将报警派发为运维工单」写入口。
-     *
-     * <p>#46 收口裁定:报警不再直接派单,改为审核转工单生成 0-待派单 工单,
-     * 再由统一工单页派发。本接口不再修改任何数据,方法签名与路由暂时保留。</p>
-     */
-    @Deprecated
-    @PostMapping("/dispatch")
-    @ApiOperation("【已停用】将报警派发为运维工单")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:dispatch')")
-    public AjaxResult dispatchToWorkOrder(@RequestBody AlarmDispatchInDTO inDTO) {
-        return AjaxResult.error(RETIRED_WRITE_ENDPOINT_MESSAGE);
-    }
-
-    @PostMapping("/audit")
-    @ApiOperation("报警审核/解除(支持意见录入与附件上传)")
-    @Log(title = "供水报警审核", businessType = BusinessType.UPDATE)
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:audit')")
-    public AjaxResult auditAlarm(@RequestBody AlarmAuditInDTO inDTO) {
-        try {
-            AlarmAuditOutDTO audit = waterSupplyAlarmService.auditAlarm(inDTO);
-            return AjaxResult.success("审核操作成功", audit);
-        } catch (Exception e) {
-            log.error("供水报警审核失败:alarmId={}", inDTO != null ? inDTO.getAlarmId() : null, e);
-            return AjaxResult.error("审核操作失败:" + e.getMessage());
-        }
-    }
-
-    @GetMapping("/audit/order-stats")
-    @ApiOperation("报警审核页工单状态计数(全部/待处理/处理中/待验收/已完成)")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
-    public AjaxResult getAuditOrderStats() {
-        return AjaxResult.success(waterSupplyAlarmService.getAuditOrderStats());
-    }
-
-    @GetMapping("/audit/list")
-    @ApiOperation("查询报警审核记录列表")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
-    public AjaxResult getAuditList(@ApiParam(value = "报警ID", required = true) @RequestParam String alarmId) {
-        return AjaxResult.success(waterSupplyAlarmService.getAuditList(alarmId));
-    }
-
-    @GetMapping("/flow/{alarmId}")
-    @ApiOperation("报警处置全流程跟踪(报警+审核记录+工单+工单操作日志)")
-    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
-    public AjaxResult getAlarmFlow(@PathVariable String alarmId) {
-        AlarmFlowOutDTO flow = waterSupplyAlarmService.getAlarmFlow(alarmId);
-        return flow != null && flow.getAlarm() != null ? AjaxResult.success(flow) : AjaxResult.error("报警信息不存在");
-    }
-
     @GetMapping("/map/points")
     @ApiOperation("监测报警一张图-报警点位(按报警级别着色)")
     @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")

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

@@ -3,7 +3,11 @@ package com.zksy.web.controller.alarm;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.service.AlarmDataService;
-import com.zksy.ops.service.WorkOrderService;
+import com.zksy.base.alarm.service.AlarmReviewService;
+import com.zksy.base.alarm.service.AlarmReviewAttachmentService;
+import com.zksy.base.alarm.dto.AlarmReviewQuery;
+import com.zksy.base.alarm.dto.AlarmReviewRequest;
+import com.zksy.base.alarm.enums.AlarmRecordStatusEnum;
 import com.zksy.common.annotation.Anonymous;
 import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
@@ -14,68 +18,95 @@ import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.transaction.annotation.Transactional;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.multipart.MultipartFile;
+import javax.validation.Valid;
 
 import java.time.LocalDateTime;
-import java.util.Map;
 
 @Slf4j
 @RestController
 @RequestMapping("/api/alarm")
-@Api(tags = "监测预警")
+@Api(tags = "监测报警")
 public class AlarmController {
 
     @Autowired
     private AlarmDataService service;
 
     @Autowired
-    private WorkOrderService businessWorkOrderService;
+    private AlarmReviewService reviewService;
+
+    @Autowired
+    private AlarmReviewAttachmentService attachmentService;
+
+    @GetMapping("/reviews")
+    @ApiOperation("报警审核列表,仅未处理报警,按设备大类及报警编码隔离")
+    @PreAuthorize("@ss.hasPermi('alarm:review:list')")
+    public AjaxResult reviews(@Valid AlarmReviewQuery query) {
+        return AjaxResult.success(reviewService.page(query));
+    }
+
+    @GetMapping("/reviews/statistics")
+    @ApiOperation("审核列表全部及待审核数量,同列表筛选范围")
+    @PreAuthorize("@ss.hasPermi('alarm:review:list')")
+    public AjaxResult reviewStatistics(@Valid AlarmReviewQuery query) {
+        return AjaxResult.success(reviewService.statistics(query));
+    }
+
+    @GetMapping("/reviews/options")
+    @ApiOperation("报警等级和审核结果枚举选项")
+    @PreAuthorize("@ss.hasPermi('alarm:review:list')")
+    public AjaxResult reviewOptions() {
+        return AjaxResult.success(reviewService.options());
+    }
+
+    @GetMapping("/reviews/{alarmId}")
+    @ApiOperation("审核报警详情")
+    @PreAuthorize("@ss.hasPermi('alarm:review:list')")
+    public AjaxResult reviewDetail(@PathVariable("alarmId") String alarmId, @Valid AlarmReviewQuery query) {
+        return AjaxResult.success(reviewService.detail(alarmId, query));
+    }
+
+    @GetMapping("/{alarmId}/logs")
+    @ApiOperation("报警处置时间线")
+    @PreAuthorize("@ss.hasPermi('alarm:review:list')")
+    public AjaxResult logs(@PathVariable("alarmId") String alarmId, @Valid AlarmReviewQuery query) {
+        return AjaxResult.success(reviewService.logs(alarmId, query));
+    }
+
+    @PostMapping("/{alarmId}/reviews")
+    @ApiOperation("提交审核;确认报警同时创建待派单工单,不修改设备状态")
+    @PreAuthorize("@ss.hasPermi('alarm:review:audit')")
+    @Log(title = "报警审核", businessType = BusinessType.UPDATE)
+    public AjaxResult review(@PathVariable("alarmId") String alarmId, @Valid AlarmReviewQuery query,
+                             @Valid @RequestBody AlarmReviewRequest request) {
+        return AjaxResult.success(reviewService.review(alarmId, query, request));
+    }
+
+    @PostMapping("/{alarmId}/attachments")
+    @ApiOperation("上传报警审核图片,仅JPG/JPEG/PNG且单张不超过10MB")
+    @PreAuthorize("@ss.hasPermi('alarm:review:audit')")
+    public AjaxResult uploadAttachment(@PathVariable("alarmId") String alarmId, @Valid AlarmReviewQuery query,
+                                       @RequestPart("file") MultipartFile file) throws Exception {
+        reviewService.detail(alarmId, query);
+        return AjaxResult.success(attachmentService.upload(alarmId, file));
+    }
 
     @GetMapping("/findByPage")
     @ApiOperation("报警分页查询")
     @Anonymous
     public AjaxResult findByPage(@ApiParam("页码") long pageNum,
                                   @ApiParam("每页条数") long pageSize,
-                                  @RequestParam(required = false) String warningType,
-                                  @RequestParam(required = false) Integer alarmStatus) {
+                                  @RequestParam(value = "warningType", required = false) String warningType,
+                                  @RequestParam(value = "alarmStatus", required = false) Integer alarmStatus) {
         Page<AlarmData> page = service.findByPage(pageNum, pageSize, null, warningType, null, alarmStatus, null, null, null);
         return AjaxResult.success(page);
     }
 
-    @PostMapping("/approve-to-work-order")
-    @ApiOperation("报警审核通过并创建运维工单")
-    @Transactional(rollbackFor = Exception.class)
-    public AjaxResult approveToWorkOrder(@RequestBody Map<String, Object> body) {
-        if (body == null || body.get("alarmId") == null) {
-            return AjaxResult.error("报警ID不能为空");
-        }
-        String alarmId = body.get("alarmId").toString();
-        AlarmData alarm = service.getById(alarmId);
-        if (alarm == null) {
-            return AjaxResult.error("报警信息不存在");
-        }
-
-        Object orderLevel = body.get("orderLevel");
-        Object orderDesc = body.get("orderDesc");
-        Integer level = null;
-        if (orderLevel != null && !orderLevel.toString().trim().isEmpty()) {
-            try {
-                level = Integer.valueOf(orderLevel.toString());
-            } catch (NumberFormatException e) {
-                return AjaxResult.error("工单优先级格式不正确");
-            }
-        }
-        Long orderId = businessWorkOrderService.createWorkOrderFromAlarm(
-                alarmId,
-                level,
-                orderDesc == null ? null : orderDesc.toString());
-        return AjaxResult.success("报警已关联工单", orderId);
-    }
-
     @GetMapping("/getById/{id}")
     @ApiOperation("根据ID查询报警")
     @Anonymous
-    public AjaxResult getById(@PathVariable String id) {
+    public AjaxResult getById(@PathVariable("id") String id) {
         AlarmData entity = service.getById(id);
         return entity != null ? AjaxResult.success(entity) : AjaxResult.error("报警信息不存在");
     }
@@ -83,14 +114,14 @@ public class AlarmController {
     @PutMapping("/handle/{alarmId}")
     @ApiOperation("处理报警")
     @Log(title = "处理报警", businessType = BusinessType.UPDATE)
-    public AjaxResult handle(@PathVariable String alarmId,
-                              @RequestParam String handleUser,
-                              @RequestParam(required = false) String handleRemark) {
+    public AjaxResult handle(@PathVariable("alarmId") String alarmId,
+                              @RequestParam("handleUser") String handleUser,
+                              @RequestParam(value = "handleRemark", required = false) String handleRemark) {
         AlarmData entity = service.getById(alarmId);
         if (entity == null) {
             return AjaxResult.error("报警信息不存在");
         }
-        entity.setAlarmStatus(1);
+        entity.setAlarmStatus(AlarmRecordStatusEnum.HANDLED.getCode());
         entity.setHandleTime(LocalDateTime.now());
         entity.setHandleUser(handleUser);
         entity.setHandleRemark(handleRemark);
@@ -103,6 +134,6 @@ public class AlarmController {
     @ApiOperation("获取未处理报警点位(报警一张图)")
     @Anonymous
     public AjaxResult getActivePoints() {
-        return AjaxResult.success(service.getAlarmDataList(null, null, null, 0));
+        return AjaxResult.success(service.getAlarmDataList(null, null, null, AlarmRecordStatusEnum.UNHANDLED.getCode()));
     }
 }

+ 0 - 35
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasMonitorController.java

@@ -1,35 +0,0 @@
-package com.zksy.web.controller.gasbasic;
-
-import com.zksy.base.service.GasMonitorService;
-import com.zksy.common.annotation.Anonymous;
-import com.zksy.common.core.domain.AjaxResult;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
-
-/**
- * 场站可燃气体浓度监测
- */
-@Api(tags = "燃气监测-场站可燃气体")
-@RestController
-@RequestMapping("/api/gas-monitor")
-@Anonymous
-public class GasMonitorController {
-
-    @Autowired
-    private GasMonitorService gasMonitorService;
-
-    @GetMapping("/points")
-    @ApiOperation("获取场站监测点列表(含最新气体浓度+温度)")
-    public AjaxResult getPoints() {
-        return AjaxResult.success(gasMonitorService.getPoints());
-    }
-
-    @GetMapping("/point/{pointId}/trend")
-    @ApiOperation("获取24小时气体浓度趋势")
-    public AjaxResult getTrend(@PathVariable String pointId,
-                               @RequestParam(defaultValue = "24") int hours) {
-        return AjaxResult.success(gasMonitorService.getTrend(pointId, hours));
-    }
-}

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

@@ -1,70 +0,0 @@
-package com.zksy.web.controller.WaterSupply.MonitorAlarm;
-
-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 static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-/**
- * 供水报警写入口收敛契约测试(#46 收口裁定)。
- *
- * <p>报警审核是供水报警唯一的写入口(转工单 / 误报解除)。旧的
- * PUT /waterSupply/alarm/resolve/{alarmId} 与 POST /waterSupply/alarm/dispatch
- * 已停用,调用不得修改任何数据;实现暂时保留,去留由后续工单决定。</p>
- */
-class WaterSupplyAlarmRetiredEndpointTest {
-
-    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 resolveEndpointIsRetiredAndTouchesNoData() throws Exception {
-        mockMvc.perform(put("/waterSupply/alarm/resolve/alarm-1").param("handleRemark", "任意备注"))
-                .andExpect(status().isOk())
-                .andExpect(result -> {
-                    String json = body(result);
-                    if (!json.contains("已停用")) {
-                        throw new AssertionError("停用接口应返回停用说明,实际:" + json);
-                    }
-                });
-        verify(alarmService, never()).resolveAlarm(any(), any());
-    }
-
-    @Test
-    void dispatchEndpointIsRetiredAndTouchesNoData() throws Exception {
-        mockMvc.perform(post("/waterSupply/alarm/dispatch")
-                        .contentType("application/json")
-                        .content("{\"alarmId\":\"alarm-1\"}"))
-                .andExpect(status().isOk())
-                .andExpect(result -> {
-                    String json = body(result);
-                    if (!json.contains("已停用")) {
-                        throw new AssertionError("停用接口应返回停用说明,实际:" + json);
-                    }
-                });
-        verify(alarmService, never()).dispatchToWorkOrder(any());
-    }
-}

+ 42 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/alarm/AlarmReviewControllerTest.java

@@ -0,0 +1,42 @@
+package com.zksy.web.controller.alarm;
+
+import com.zksy.base.alarm.dto.AlarmReviewQuery;
+import com.zksy.base.alarm.service.AlarmReviewService;
+import com.zksy.web.controller.WaterSupply.MonitorAlarm.WaterSupplyAlarmController;
+import org.junit.jupiter.api.*;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class AlarmReviewControllerTest {
+    private MockMvc mvc;
+    private final AlarmReviewService reviews = mock(AlarmReviewService.class);
+    @BeforeEach void setup() {
+        AlarmController controller = new AlarmController();
+        ReflectionTestUtils.setField(controller, "reviewService", reviews);
+        mvc = MockMvcBuilders.standaloneSetup(controller, new WaterSupplyAlarmController()).build();
+    }
+    @Test void fullDayEndTimeBindsWithMicrosecondPrecision() throws Exception {
+        mvc.perform(get("/api/alarm/reviews").param("rootTypeId","1").param("endTime","2026-09-23 23:59:59.999999")).andExpect(status().isOk());
+        ArgumentCaptor<AlarmReviewQuery> query = ArgumentCaptor.forClass(AlarmReviewQuery.class);
+        verify(reviews).page(query.capture()); assertEquals(999999000, query.getValue().getEndTime().getNano());
+    }
+    @Test void missingRootAndEmptyOpinionAreRejected() throws Exception {
+        mvc.perform(get("/api/alarm/reviews")).andExpect(status().isBadRequest());
+        mvc.perform(post("/api/alarm/a1/reviews").param("rootTypeId","1").contentType("application/json")
+                .content("{\"auditStatus\":1,\"opinion\":\" \"}")).andExpect(status().isBadRequest());
+        verifyNoInteractions(reviews);
+    }
+    @Test void removedWriteEndpointsAreNoLongerRegistered() throws Exception {
+        mvc.perform(post("/api/alarm/approve-to-work-order").contentType("application/json").content("{}")).andExpect(status().isNotFound());
+        mvc.perform(put("/waterSupply/alarm/resolve/a1")).andExpect(status().isNotFound());
+        mvc.perform(post("/waterSupply/alarm/dispatch")).andExpect(status().isNotFound());
+        mvc.perform(post("/waterSupply/alarm/audit")).andExpect(status().isNotFound());
+        verifyNoInteractions(reviews);
+    }
+}

+ 0 - 121
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/domain/AlarmAudit.java

@@ -1,121 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm.domain;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-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.time.LocalDateTime;
-
-/**
- * 供水监测报警审核记录实体
- */
-@Data
-@TableName(value = "alarm_audit", schema = "app_user")
-@ApiModel(
-        value = "AlarmAudit",
-        description = "报警审核记录,包含报警审核/解除操作、意见、附件及全流程跟踪所需字段"
-)
-public class AlarmAudit implements Serializable {
-
-    @ApiModelProperty(
-            value = "审核记录ID",
-            example = "1",
-            position = 1,
-            notes = "数据库自增主键,新增时不需要传入"
-    )
-    @TableId(type = IdType.AUTO)
-    private Long auditId;
-
-    @ApiModelProperty(
-            value = "关联报警ID",
-            example = "uuid",
-            required = true,
-            position = 2
-    )
-    private String alarmId;
-
-    @ApiModelProperty(
-            value = "审核动作",
-            example = "1",
-            required = true,
-            allowableValues = "1,2",
-            position = 3,
-            notes = "1-审核 2-解除"
-    )
-    private Integer auditAction;
-
-    @ApiModelProperty(
-            value = "审核结果",
-            example = "1",
-            required = true,
-            allowableValues = "1,2",
-            position = 4,
-            notes = "1-属实/审核通过 2-误报/解除报警"
-    )
-    private Integer auditResult;
-
-    @ApiModelProperty(
-            value = "审核/解除意见",
-            example = "现场核实确为管网压力异常,同意派发工单处理",
-            position = 5
-    )
-    private String auditOpinion;
-
-    @ApiModelProperty(
-            value = "附件地址(多个用逗号分隔)",
-            example = "https://xx/upload/a.jpg,https://xx/upload/b.jpg",
-            position = 6
-    )
-    private String attachUrl;
-
-    @ApiModelProperty(
-            value = "操作人ID",
-            example = "1",
-            hidden = true,
-            position = 7
-    )
-    private Long auditUserId;
-
-    @ApiModelProperty(
-            value = "操作人姓名",
-            example = "admin",
-            hidden = true,
-            position = 8
-    )
-    private String auditUserName;
-
-    @ApiModelProperty(
-            value = "操作时间",
-            example = "2026-08-11 10:00:00",
-            position = 9
-    )
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime auditTime;
-
-    @ApiModelProperty(
-            value = "创建时间",
-            example = "2026-08-11 10:00:00",
-            hidden = true,
-            position = 10
-    )
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime createTime;
-
-    @ApiModelProperty(
-            value = "更新时间",
-            example = "2026-08-11 10:00:00",
-            hidden = true,
-            position = 11
-    )
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime updateTime;
-
-    @TableField(exist = false)
-    private static final long serialVersionUID = 1L;
-}

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

@@ -1,44 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm.dto.in;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.io.Serializable;
-
-/**
- * 报警审核-入参
- */
-@Data
-@ApiModel(value = "报警审核-入参", description = "报警审核/解除操作入参")
-public class AlarmAuditInDTO implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @ApiModelProperty(value = "报警ID", required = true, example = "uuid")
-    private String alarmId;
-
-    @ApiModelProperty(
-            value = "审核动作(报警审核页只有这两个动作)",
-            required = true,
-            allowableValues = "1,2",
-            example = "1",
-            notes = "1-转工单 2-误报解除;其它取值被后端拒绝"
-    )
-    private Integer auditAction;
-
-    @ApiModelProperty(
-            value = "审核结果",
-            required = true,
-            allowableValues = "1,2",
-            example = "1",
-            notes = "1-属实/审核通过(转工单) 2-误报/解除报警;结果为 2 时一律按误报解除处理,不创建工单"
-    )
-    private Integer auditResult;
-
-    @ApiModelProperty(value = "审核/解除意见", example = "现场核实确为管网压力异常,同意派发工单处理")
-    private String auditOpinion;
-
-    @ApiModelProperty(value = "附件地址(多个用逗号分隔)", example = "https://xx/upload/a.jpg")
-    private String attachUrl;
-}

+ 0 - 48
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/AlarmDispatchInDTO.java

@@ -1,48 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm.dto.in;
-
-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.util.Date;
-
-/**
- * 报警派发工单-入参
- */
-@Data
-@ApiModel(value = "报警派发工单-入参", description = "将供水监测报警派发为运维工单的入参")
-public class AlarmDispatchInDTO implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @ApiModelProperty(value = "报警ID", required = true, example = "uuid")
-    private String alarmId;
-
-    @ApiModelProperty(
-            value = "工单类型",
-            example = "1",
-            allowableValues = "1,2,3",
-            notes = "1-故障维修(默认) 2-日常巡检 3-设备保养"
-    )
-    private Integer orderType;
-
-    @ApiModelProperty(
-            value = "工单优先级",
-            example = "1",
-            allowableValues = "1,2,3",
-            notes = "1-紧急 2-一般 3-低;不传时按报警等级映射(1级→紧急,2/3级→一般,4级→低)"
-    )
-    private Integer orderLevel;
-
-    @ApiModelProperty(value = "工单问题描述,不传时取报警信息", example = "供水管网压力异常报警,请现场核实处理")
-    private String orderDesc;
-
-    @ApiModelProperty(value = "负责部门ID")
-    private Long deptId;
-
-    @ApiModelProperty(value = "计划完成时间")
-    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private Date planFinishTime;
-}

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

@@ -1,35 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm.dto.out;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.io.Serializable;
-
-/**
- * 报警审核页工单状态统计-出参
- *
- * <p>统计范围:供水设备(按设备类型树判定)且由报警审核生成的工单(alarm_id 非空)。
- * 分组口径与统一工单页保持一致:待处理 0/1,处理中 2/3/6,待验收 4,已完成 5/7。</p>
- */
-@Data
-@ApiModel(value = "报警审核工单状态统计", description = "报警审核页顶部状态计数条")
-public class AlarmAuditOrderStatVO implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @ApiModelProperty(value = "全部报警工单数", example = "16")
-    private long total;
-
-    @ApiModelProperty(value = "待处理(0-待派单、1-待接单)", example = "3")
-    private long pendingCount;
-
-    @ApiModelProperty(value = "处理中(2-已接单、3-处理中、6-延期中)", example = "4")
-    private long processingCount;
-
-    @ApiModelProperty(value = "待验收(4-待验收)", example = "3")
-    private long verifyingCount;
-
-    @ApiModelProperty(value = "已完成(5-已结案、7-已取消)", example = "6")
-    private long finishedCount;
-}

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

@@ -1,53 +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.time.LocalDateTime;
-
-/**
- * 报警审核记录-出参
- */
-@Data
-@ApiModel(value = "报警审核记录-出参", description = "报警审核/解除操作记录")
-public class AlarmAuditOutDTO implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @ApiModelProperty(value = "审核记录ID")
-    private Long auditId;
-
-    @ApiModelProperty(value = "报警ID")
-    private String alarmId;
-
-    @ApiModelProperty(value = "审核动作:1-审核 2-解除")
-    private Integer auditAction;
-
-    @ApiModelProperty(value = "审核动作文本", example = "审核")
-    private String auditActionText;
-
-    @ApiModelProperty(value = "审核结果:1-属实/审核通过 2-误报/解除报警")
-    private Integer auditResult;
-
-    @ApiModelProperty(value = "审核结果文本", example = "属实")
-    private String auditResultText;
-
-    @ApiModelProperty(value = "审核/解除意见")
-    private String auditOpinion;
-
-    @ApiModelProperty(value = "附件地址(多个用逗号分隔)")
-    private String attachUrl;
-
-    @ApiModelProperty(value = "操作人ID")
-    private Long auditUserId;
-
-    @ApiModelProperty(value = "操作人姓名")
-    private String auditUserName;
-
-    @ApiModelProperty(value = "操作时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime auditTime;
-}

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

@@ -1,32 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm.dto.out;
-
-import com.zksy.ops.domain.vo.WorkOrderLogVO;
-import com.zksy.ops.domain.vo.WorkOrderVO;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.List;
-
-/**
- * 报警处置全流程跟踪-出参
- */
-@Data
-@ApiModel(value = "报警处置全流程跟踪-出参", description = "报警信息 + 审核记录 + 关联工单及操作日志")
-public class AlarmFlowOutDTO implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @ApiModelProperty(value = "报警信息(含设备信息)")
-    private WaterAlarmOutDTO alarm;
-
-    @ApiModelProperty(value = "审核/解除记录列表")
-    private List<AlarmAuditOutDTO> auditList;
-
-    @ApiModelProperty(value = "关联工单(可能为空;责任人/派单人姓名与设备信息需调用方按需回填)")
-    private WorkOrderVO workOrder;
-
-    @ApiModelProperty(value = "工单操作日志列表")
-    private List<WorkOrderLogVO> workOrderLogList;
-}

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/WaterAlarmOutDTO.java

@@ -78,7 +78,7 @@ public class WaterAlarmOutDTO implements Serializable {
     @ApiModelProperty(value = "设备报警状态文本", example = "报警中")
     private String deviceAlarmStatusText;
 
-    @ApiModelProperty(value = "报警展示状态:1-待审核 2-已转工单 3-已解除(后端推导,不等同报警记录状态)", example = "2")
+    @ApiModelProperty(value = "报警展示状态:1-待审核 2-已转工单 3-已解除 4-误报警(后端推导,不等同报警记录状态)", example = "2")
     private Integer displayStatus;
 
     @ApiModelProperty(value = "报警展示状态文本", example = "已转工单")

+ 0 - 13
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/mapper/AlarmAuditMapper.java

@@ -1,13 +0,0 @@
-package com.zksy.WaterSupply.MonitorAlarm.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * 报警审核记录 Mapper
- */
-@Mapper
-public interface AlarmAuditMapper extends BaseMapper<AlarmAudit> {
-
-}

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

@@ -1,12 +1,7 @@
 package com.zksy.WaterSupply.MonitorAlarm.service;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmAuditInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmDispatchInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOrderStatVO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmDashboardOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmFlowOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
@@ -72,38 +67,6 @@ public interface IWaterSupplyAlarmService {
      */
     WaterAlarmOutDTO getAlarmDetail(String alarmId);
 
-    /**
-     * 一键标记已处理
-     */
-    boolean resolveAlarm(String alarmId, String handleRemark);
-
-    /**
-     * 将报警派发为运维工单
-     */
-    boolean dispatchToWorkOrder(AlarmDispatchInDTO inDTO);
-
-    /**
-     * 报警审核/解除(意见 + 附件)
-     */
-    AlarmAuditOutDTO auditAlarm(AlarmAuditInDTO inDTO);
-
-    /**
-     * 报警审核页工单状态计数:全部 / 待处理 / 处理中 / 待验收 / 已完成。
-     *
-     * <p>只统计供水设备且由报警审核生成(alarm_id 非空)的工单。</p>
-     */
-    AlarmAuditOrderStatVO getAuditOrderStats();
-
-    /**
-     * 查询报警审核记录列表
-     */
-    List<AlarmAuditOutDTO> getAuditList(String alarmId);
-
-    /**
-     * 报警处置全流程跟踪(报警 + 审核记录 + 工单 + 工单日志)
-     */
-    AlarmFlowOutDTO getAlarmFlow(String alarmId);
-
     /**
      * 监测报警一张图点位(未处理报警,含经纬度与报警级别)
      */

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

@@ -5,49 +5,34 @@ 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.domain.AlarmAudit;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmAuditInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmDispatchInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmDashboardOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmFlowOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOrderStatVO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
 import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyAlarmService;
 import com.zksy.base.service.impl.WaterSupplyQueryService;
 import com.zksy.base.alarm.domain.AlarmData;
-import com.zksy.base.alarm.service.AlarmClearService;
+import com.zksy.base.alarm.enums.AlarmAuditStatusEnum;
+import com.zksy.base.alarm.enums.AlarmDisplayStatusEnum;
+import com.zksy.base.alarm.enums.AlarmLevelEnum;
+import com.zksy.base.alarm.enums.AlarmRecordStatusEnum;
+import com.zksy.ops.enums.WorkOrderStatusEnum;
 import com.zksy.base.alarm.service.AlarmDataService;
 import com.zksy.base.domain.EquipmentBase;
 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.ops.domain.WorkOrderLog;
-import com.zksy.ops.domain.vo.WorkOrderLogVO;
-import com.zksy.ops.domain.vo.WorkOrderVO;
-import com.zksy.ops.enums.WorkOrderLogOperTypeEnum;
-import com.zksy.ops.enums.WorkOrderStatusEnum;
 import com.zksy.base.mapper.EquipmentBaseMapper;
-import com.zksy.ops.mapper.WorkOrderLogMapper;
 import com.zksy.ops.mapper.WorkOrderMapper;
-import com.zksy.common.core.domain.model.LoginUser;
 import com.zksy.common.exception.ServiceException;
-import com.zksy.common.utils.SecurityUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
-import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -55,7 +40,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Random;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -67,13 +51,10 @@ import java.util.stream.Collectors;
 @Service
 public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
 
-    private static final DateTimeFormatter ORDER_NO_FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
 
     @Autowired
     private AlarmDataService alarmDataService;
 
-    @Autowired
-    private AlarmClearService alarmClearService;
 
     @Autowired
     private WaterSupplyQueryService waterSupplyQueryService;
@@ -84,44 +65,15 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     @Autowired
     private WorkOrderMapper workOrderMapper;
 
-    @Autowired
-    private WorkOrderLogMapper workOrderLogMapper;
 
-    @Autowired
-    private AlarmAuditMapper alarmAuditMapper;
 
     @Autowired
     private EquipmentTypeMapper equipmentTypeMapper;
 
     /** 报警展示状态:1-待审核 2-已转工单 3-已解除 */
-    private static final int DISPLAY_PENDING_AUDIT = 1;
-    private static final int DISPLAY_TRANSFERRED = 2;
-    private static final int DISPLAY_RELEASED = 3;
-
-    /** 审核动作:1-转工单 2-误报解除(报警审核页仅提供这两个动作) */
-    private static final int AUDIT_ACTION_TRANSFER = 1;
-    private static final int AUDIT_ACTION_FALSE_ALARM_RELEASE = 2;
-
-    /**
-     * 审核页计数条分组。状态码一律取自权威枚举 WorkOrderStatusEnum(ADR-0010),
-     * 不写字面量,也不以前端字典为准;未办结工单集合直接用 activeCodes()。
-     */
-    private static final List<Integer> GROUP_PENDING = Arrays.asList(
-            WorkOrderStatusEnum.PENDING_DISPATCH.getCode(),
-            WorkOrderStatusEnum.PENDING_ACCEPT.getCode());
-    private static final List<Integer> GROUP_PROCESSING = Arrays.asList(
-            WorkOrderStatusEnum.ACCEPTED.getCode(),
-            WorkOrderStatusEnum.PROCESSING.getCode(),
-            WorkOrderStatusEnum.DELAYING.getCode());
-    private static final List<Integer> GROUP_VERIFYING = Collections.singletonList(
-            WorkOrderStatusEnum.PENDING_VERIFY.getCode());
-    private static final List<Integer> GROUP_FINISHED = WorkOrderStatusEnum.finishedCodes();
-
-    /** 审核结果:1-属实/审核通过 2-误报/解除报警 */
-    private static final int AUDIT_RESULT_FALSE_ALARM = 2;
-
-    private static final String SAME_TYPE_ORDER_EXISTS =
-            "该设备同一报警类型已存在未结束的故障维修工单,不能重复创建";
+    private static final int DISPLAY_PENDING_AUDIT = AlarmDisplayStatusEnum.PENDING.getCode();
+    private static final int DISPLAY_TRANSFERRED = AlarmDisplayStatusEnum.TRANSFERRED.getCode();
+    private static final int DISPLAY_RELEASED = AlarmDisplayStatusEnum.RELEASED.getCode();
 
     // ------------------------------------------------------------------
     // 供水设备范围解析
@@ -333,7 +285,6 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
                 .filter(StrUtil::isNotBlank)
                 .collect(Collectors.toSet()));
         Map<String, WorkOrder> orderMap = loadWorkOrderMapByAlarmIds(alarmIds);
-        Map<String, AlarmAudit> latestAuditMap = loadLatestAuditMap(alarmIds);
         Map<String, EquipmentStatus> statusMap = loadStatusMap(eqMap.values());
         List<WaterAlarmOutDTO> vos = new ArrayList<>();
         for (AlarmData alarm : alarms) {
@@ -359,10 +310,10 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
                 vo.setOrderNo(order.getOrderNo());
                 vo.setOrderStatus(order.getOrderStatus());
             }
-            int deviceAlarm = resolveDeviceAlarmStatus(alarm, eq, statusMap, latestAuditMap.get(alarm.getId()));
+            int deviceAlarm = resolveDeviceAlarmStatus(alarm, eq, statusMap);
             vo.setDeviceAlarmStatus(deviceAlarm);
             vo.setDeviceAlarmStatusText(deviceAlarm == 1 ? "报警中" : "正常");
-            int display = resolveDisplayStatus(alarm, order, latestAuditMap.get(alarm.getId()));
+            int display = resolveDisplayStatus(alarm, order);
             vo.setDisplayStatus(display);
             vo.setDisplayStatusText(displayStatusText(display));
             vos.add(vo);
@@ -371,62 +322,39 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     }
 
     /**
-     * 设备报警状态:以设备状态表为准;无状态记录时回退到报警记录是否未处理,
-     * 但经审核判定为误报解除的报警不再视为设备报警中。
+     * 设备报警状态以设备状态表为准;无记录时回退到报警记录状态,不从审核结果推断设备恢复。
      */
     private int resolveDeviceAlarmStatus(AlarmData alarm, EquipmentBase eq,
-                                         Map<String, EquipmentStatus> statusMap, AlarmAudit audit) {
+                                         Map<String, EquipmentStatus> statusMap) {
         if (eq != null && eq.getEquipmentId() != null) {
             EquipmentStatus status = statusMap.get(eq.getEquipmentId());
             if (status != null && status.getAlarmStatus() != null) {
                 return status.getAlarmStatus();
             }
         }
-        if (audit != null && (Integer.valueOf(2).equals(audit.getAuditResult())
-                || Integer.valueOf(2).equals(audit.getAuditAction()))) {
-            return 0;
-        }
         return alarm.getAlarmStatus() != null && alarm.getAlarmStatus() == 0 ? 1 : 0;
     }
 
     /**
      * 报警展示状态推导:
-     * 关联工单未取消 -> 已转工单;否则报警记录已处理或最新审核为误报 -> 已解除;其余 -> 待审核。
-     * 工单已取消代表处置已撤销,按报警事实自身归档为已解除,展示口径由后端统一。
+     * 关联工单未取消 -> 已转工单;报警已处理 -> 已解除;误报审核 -> 误报警。
+     * 已确认的报警即使工单取消,也不能重新显示成待审核或已恢复。
      */
-    private int resolveDisplayStatus(AlarmData alarm, WorkOrder order, AlarmAudit audit) {
-        boolean orderAlive = order != null && !Integer.valueOf(7).equals(order.getOrderStatus());
-        if (orderAlive) {
+    private int resolveDisplayStatus(AlarmData alarm, WorkOrder order) {
+        boolean orderAlive = order != null && !WorkOrderStatusEnum.CANCELLED.getCode().equals(order.getOrderStatus());
+        if (orderAlive || Objects.equals(alarm.getAuditStatus(), AlarmAuditStatusEnum.CONFIRMED.getCode())) {
             return DISPLAY_TRANSFERRED;
         }
-        boolean alarmHandled = alarm.getAlarmStatus() != null && alarm.getAlarmStatus() == 1;
-        boolean falseAlarm = audit != null && Integer.valueOf(2).equals(audit.getAuditResult());
-        return alarmHandled || falseAlarm ? DISPLAY_RELEASED : DISPLAY_PENDING_AUDIT;
+        boolean alarmHandled = Objects.equals(alarm.getAlarmStatus(), AlarmRecordStatusEnum.HANDLED.getCode());
+        boolean falseAlarm = Objects.equals(alarm.getAuditStatus(), AlarmAuditStatusEnum.FALSE_ALARM.getCode());
+        if (alarmHandled) return DISPLAY_RELEASED;
+        if (falseAlarm) return AlarmDisplayStatusEnum.FALSE_ALARM.getCode();
+        return DISPLAY_PENDING_AUDIT;
     }
 
     private String displayStatusText(int displayStatus) {
-        switch (displayStatus) {
-            case DISPLAY_TRANSFERRED:
-                return "已转工单";
-            case DISPLAY_RELEASED:
-                return "已解除";
-            default:
-                return "待审核";
-        }
-    }
-
-    /**
-     * 报警ID -> 最新审核记录
-     */
-    private Map<String, AlarmAudit> loadLatestAuditMap(List<String> alarmIds) {
-        if (CollUtil.isEmpty(alarmIds)) {
-            return Collections.emptyMap();
-        }
-        List<AlarmAudit> audits = alarmAuditMapper.selectList(new LambdaQueryWrapper<AlarmAudit>()
-                .in(AlarmAudit::getAlarmId, alarmIds)
-                .orderByDesc(AlarmAudit::getAuditId));
-        return audits.stream()
-                .collect(Collectors.toMap(AlarmAudit::getAlarmId, Function.identity(), (a, b) -> a));
+        AlarmDisplayStatusEnum value = AlarmDisplayStatusEnum.fromCode(displayStatus);
+        return value == null ? "未知状态" : value.getLabel();
     }
 
     private Map<String, EquipmentStatus> loadStatusMap(Collection<EquipmentBase> devices) {
@@ -627,366 +555,8 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     }
 
     private String formatAlarmLevel(Integer alarmLevel) {
-        if (alarmLevel == null) {
-            return "";
-        }
-        switch (alarmLevel) {
-            case 1:
-                return "一级报警";
-            case 2:
-                return "二级报警";
-            case 3:
-                return "三级报警";
-            case 4:
-                return "四级报警";
-            default:
-                return "报警";
-        }
-    }
-
-    // ------------------------------------------------------------------
-    // 处理 / 派发工单
-    // ------------------------------------------------------------------
-
-    @Override
-    public boolean resolveAlarm(String alarmId, String handleRemark) {
-        AlarmData alarm = alarmDataService.getById(alarmId);
-        if (alarm == null) {
-            throw new ServiceException("报警信息不存在");
-        }
-        alarm.setAlarmStatus(1);
-        alarm.setHandleTime(LocalDateTime.now());
-        alarm.setHandleUser(currentUserName());
-        alarm.setHandleRemark(StrUtil.blankToDefault(handleRemark, "标记已处理"));
-        alarm.setUpdateTime(LocalDateTime.now());
-        return alarmDataService.updateById(alarm);
-    }
-
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public boolean dispatchToWorkOrder(AlarmDispatchInDTO inDTO) {
-        if (inDTO == null || StrUtil.isBlank(inDTO.getAlarmId())) {
-            throw new ServiceException("报警ID不能为空");
-        }
-        AlarmData alarm = alarmDataService.getById(inDTO.getAlarmId());
-        if (alarm == null) {
-            throw new ServiceException("报警信息不存在");
-        }
-
-        // 同类报警工单位一性:同设备同报警类型只允许一张未办结工单
-        if (alarm.getOrderId() != null) {
-            if (workOrderMapper.selectById(alarm.getOrderId()) != null) {
-                return true;
-            }
-            throw new ServiceException("报警关联的工单不存在,请先检查关联数据");
-        }
-        assertNoUnfinishedSameTypeOrder(alarm);
-
-        // 工单只保存设备主键;设备编码由 equipment_base 关联查询。
-        EquipmentBase equipment = alarm.getDeviceCode() == null ? null
-                : equipmentBaseMapper.selectOne(new LambdaQueryWrapper<EquipmentBase>()
-                        .eq(EquipmentBase::getEquipmentCode, alarm.getDeviceCode())
-                        .last("LIMIT 1"));
-
-        WorkOrder workOrder = new WorkOrder();
-        workOrder.setOrderNo(generateOrderNo());
-        if (equipment == null) {
-            throw new ServiceException("报警设备未关联设备档案,无法创建工单");
-        }
-        workOrder.setDeviceId(equipment.getEquipmentId());
-        workOrder.setOrderLevel(inDTO.getOrderLevel() == null
-                ? mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()) : inDTO.getOrderLevel());
-        workOrder.setOrderDesc(StrUtil.isNotBlank(inDTO.getOrderDesc()) ? inDTO.getOrderDesc()
-                : buildDefaultOrderDesc(alarm));
-        workOrder.setDeptId(inDTO.getDeptId());
-        // 与统一工单手动创建路径一致:数据按部门隔离,缺省时归属当前用户部门,
-        // 否则部门范围过滤会让这张报警工单在统一工单页查不到,闭环断裂。
-        if (workOrder.getDeptId() == null) {
-            workOrder.setDeptId(currentDeptId());
-        }
-        workOrder.setPlanFinishTime(inDTO.getPlanFinishTime());
-        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
-        workOrder.setCreateTime(new java.util.Date());
-        workOrder.setUpdateTime(new java.util.Date());
-        workOrderMapper.insert(workOrder);
-        alarm.setOrderId(workOrder.getOrderId());
-
-        // 写工单操作日志(0-创建工单)
-        WorkOrderLog workOrderLog = new WorkOrderLog();
-        workOrderLog.setOrderId(workOrder.getOrderId());
-        workOrderLog.setOperType(0);
-        workOrderLog.setOperDesc("由供水监测报警[" + alarm.getWarningType() + "]派发工单:" + workOrder.getOrderNo());
-        workOrderLog.setOperUserId(currentUserId());
-        workOrderLog.setOperUserName(currentUserName());
-        workOrderLog.setOperTime(new java.util.Date());
-        workOrderLogMapper.insert(workOrderLog);
-
-        // 回写报警处理信息
-        alarm.setAlarmStatus(1);
-        alarm.setHandleTime(LocalDateTime.now());
-        alarm.setHandleUser(currentUserName());
-        alarm.setHandleRemark("已派发工单:" + workOrder.getOrderNo());
-        alarm.setUpdateTime(LocalDateTime.now());
-        if (!alarmDataService.update(alarm, new LambdaQueryWrapper<AlarmData>()
-                .eq(AlarmData::getId, alarm.getId()).isNull(AlarmData::getOrderId))) {
-            throw new ServiceException("报警已关联工单或已被删除,请刷新后重试");
-        }
-        return true;
-    }
-
-    /**
-     * 报警等级 -> 工单优先级:1级→紧急(0),2/3级→一般(1),4级→低(2)
-     */
-    private Integer mapAlarmLevelToOrderLevel(Integer alarmLevel) {
-        if (alarmLevel == null || alarmLevel == 1) {
-            return 0;
-        }
-        if (alarmLevel == 4) {
-            return 2;
-        }
-        return 1;
-    }
-
-    /**
-     * 报警审核页工单状态计数:只统计供水设备且由报警生成的工单。
-     *
-     * <p>分组口径以权威枚举 WorkOrderStatusEnum 为准(ADR-0010),前端字典只是同一口径的镜像,
-     * 不作为真相源,避免同一工单在两个页面被算成不同状态。</p>
-     */
-    @Override
-    public AlarmAuditOrderStatVO getAuditOrderStats() {
-        AlarmAuditOrderStatVO stats = new AlarmAuditOrderStatVO();
-        Set<String> waterDeviceIds = waterSupplyQueryService.waterDeviceIds();
-        if (waterDeviceIds.isEmpty()) {
-            return stats;
-        }
-        // 复用统一工单既有的状态聚合查询(返回 status / cnt),不再自造 group by SQL 片段
-        List<Map<String, Object>> rows = workOrderMapper.countByStatus(new LambdaQueryWrapper<WorkOrder>()
-                .exists("SELECT 1 FROM app_user.alarm_data a WHERE a.order_id = app_user.work_order.order_id")
-                .in(WorkOrder::getDeviceId, waterDeviceIds));
-        long total = 0L;
-        for (Map<String, Object> row : rows) {
-            Long count = asLong(row.get("cnt"));
-            Integer status = asInt(row.get("status"));
-            if (count == null || count == 0L || status == null || !WorkOrderStatusEnum.isValid(status)) {
-                // 非法/未知状态既不计入任何分组,也不计入总数,保证「全部 = 四个分组之和」
-                continue;
-            }
-            if (GROUP_PENDING.contains(status)) {
-                stats.setPendingCount(stats.getPendingCount() + count);
-            } else if (GROUP_PROCESSING.contains(status)) {
-                stats.setProcessingCount(stats.getProcessingCount() + count);
-            } else if (GROUP_VERIFYING.contains(status)) {
-                stats.setVerifyingCount(stats.getVerifyingCount() + count);
-            } else if (GROUP_FINISHED.contains(status)) {
-                stats.setFinishedCount(stats.getFinishedCount() + count);
-            }
-            total += count;
-        }
-        stats.setTotal(total);
-        return stats;
-    }
-
-    private Long asLong(Object value) {
-        return value instanceof Number ? ((Number) value).longValue() : null;
-    }
-
-    private Integer asInt(Object value) {
-        return value instanceof Number ? ((Number) value).intValue() : null;
-    }
-
-    /**
-     * 同类报警工单位一性:同一设备、同一报警类型下,只允许存在一张未办结的故障维修工单。
-     *
-     * <p>工单流程重构后报警与工单通过 alarm_data.order_id 关联;因此唯一性按
-     * “同设备 + 同报警类型的报警记录集合 + 未办结工单”判定,
-     * 而不是只看当前这一条报警。</p>
-     */
-    private void assertNoUnfinishedSameTypeOrder(AlarmData alarm) {
-        if (StrUtil.isBlank(alarm.getDeviceCode()) || StrUtil.isBlank(alarm.getWarningType())) {
-            throw new ServiceException("报警记录缺少设备编码或报警类型,无法判定同类工单");
-        }
-        List<Long> orderIds = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
-                        .select(AlarmData::getOrderId)
-                        .eq(AlarmData::getDeviceCode, alarm.getDeviceCode())
-                        .eq(AlarmData::getWarningType, alarm.getWarningType())
-                        .isNotNull(AlarmData::getOrderId)).stream()
-                .map(AlarmData::getOrderId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
-        if (orderIds.isEmpty()) return;
-        long unfinished = workOrderMapper.selectCount(new LambdaQueryWrapper<WorkOrder>()
-                .in(WorkOrder::getOrderId, orderIds)
-                .in(WorkOrder::getOrderStatus, WorkOrderStatusEnum.activeCodes()));
-        if (unfinished > 0) {
-            throw new ServiceException(SAME_TYPE_ORDER_EXISTS);
-        }
-    }
-
-    /**
-     * 审核动作白名单:只允许“转工单”与“误报解除”。
-     */
-    private void validateAuditAction(Integer auditAction) {
-        if (auditAction == null) {
-            throw new ServiceException("审核动作不能为空");
-        }
-        if (auditAction != AUDIT_ACTION_TRANSFER && auditAction != AUDIT_ACTION_FALSE_ALARM_RELEASE) {
-            throw new ServiceException("审核动作仅支持 1-转工单 或 2-误报解除");
-        }
-    }
-
-    /**
-     * 是否为“误报解除”:动作=2 或 结果=2 都按解除处理,不创建工单。
-     */
-    private boolean isFalseAlarmRelease(AlarmAuditInDTO inDTO) {
-        return Objects.equals(inDTO.getAuditResult(), AUDIT_RESULT_FALSE_ALARM)
-                || Objects.equals(inDTO.getAuditAction(), AUDIT_ACTION_FALSE_ALARM_RELEASE);
-    }
-
-    private String buildDefaultOrderDesc(AlarmData alarm) {
-        StringBuilder sb = new StringBuilder();
-        sb.append(StrUtil.blankToDefault(alarm.getWarningType(), "监测报警"));
-        sb.append(",当前值:").append(alarm.getActualValue() == null ? "-" : alarm.getActualValue().toPlainString());
-        if (alarm.getMinValue() != null || alarm.getMaxValue() != null) {
-            sb.append(",阈值范围:").append(alarm.getMinValue() == null ? "-" : alarm.getMinValue().toPlainString())
-                    .append("~").append(alarm.getMaxValue() == null ? "-" : alarm.getMaxValue().toPlainString());
-        }
-        sb.append(",请及时现场核实处理");
-        return sb.toString();
-    }
-
-    /**
-     * 生成工单编号:WO + yyyyMMddHHmmss + 4位随机数
-     */
-    private String generateOrderNo() {
-        return "WO" + LocalDateTime.now().format(ORDER_NO_FMT)
-                + String.format("%04d", new Random().nextInt(10000));
-    }
-
-    // ------------------------------------------------------------------
-    // 报警审核 / 全流程跟踪
-    // ------------------------------------------------------------------
-
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public AlarmAuditOutDTO auditAlarm(AlarmAuditInDTO inDTO) {
-        if (inDTO == null || StrUtil.isBlank(inDTO.getAlarmId())) {
-            throw new ServiceException("报警ID不能为空");
-        }
-        validateAuditAction(inDTO.getAuditAction());
-        if (inDTO.getAuditResult() == null) {
-            throw new ServiceException("审核结果不能为空");
-        }
-        AlarmData alarm = alarmDataService.getById(inDTO.getAlarmId());
-        if (alarm == null) {
-            throw new ServiceException("报警信息不存在");
-        }
-
-        // 转工单分支先校验同类工单位一性,避免失败后留下半截审核记录
-        if (!isFalseAlarmRelease(inDTO)) {
-            assertNoUnfinishedSameTypeOrder(alarm);
-        }
-
-        AlarmAudit audit = new AlarmAudit();
-        audit.setAlarmId(inDTO.getAlarmId());
-        audit.setAuditAction(inDTO.getAuditAction());
-        audit.setAuditResult(inDTO.getAuditResult());
-        audit.setAuditOpinion(inDTO.getAuditOpinion());
-        audit.setAttachUrl(inDTO.getAttachUrl());
-        audit.setAuditUserId(currentUserId());
-        audit.setAuditUserName(currentUserName());
-        audit.setAuditTime(LocalDateTime.now());
-        audit.setCreateTime(LocalDateTime.now());
-        audit.setUpdateTime(LocalDateTime.now());
-        alarmAuditMapper.insert(audit);
-
-        // 审核结果=误报 或 动作=误报解除 时,直接关闭报警,且不创建工单
-        boolean releaseAlarm = isFalseAlarmRelease(inDTO);
-        if (releaseAlarm) {
-            alarm.setAlarmStatus(1);
-            alarm.setHandleTime(LocalDateTime.now());
-            alarm.setHandleUser(audit.getAuditUserName());
-            alarm.setHandleRemark("误报解除:" + StrUtil.blankToDefault(inDTO.getAuditOpinion(), "经审核确认为误报"));
-            alarm.setUpdateTime(LocalDateTime.now());
-            alarmDataService.updateById(alarm);
-            // 误报解除只是触发清警复核的时机:是否清警由运行系统汇总有效报警原因后决定
-            // (ADR-0002)。仍有其他有效原因时复核会保留设备报警状态(#51)。
-            alarmClearService.review(alarm.getDeviceCode(), "误报解除");
-        } else {
-            // 审核通过即上传至运维工单,工单默认待派单状态
-            AlarmDispatchInDTO dispatch = new AlarmDispatchInDTO();
-            dispatch.setAlarmId(alarm.getId());
-            dispatch.setOrderLevel(mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()));
-            dispatch.setOrderDesc(StrUtil.isBlank(inDTO.getAuditOpinion())
-                    ? null : inDTO.getAuditOpinion());
-            dispatchToWorkOrder(dispatch);
-        }
-        return toAuditOutDTO(audit);
-    }
-
-    @Override
-    public List<AlarmAuditOutDTO> getAuditList(String alarmId) {
-        List<AlarmAudit> audits = alarmAuditMapper.selectList(new LambdaQueryWrapper<AlarmAudit>()
-                .eq(AlarmAudit::getAlarmId, alarmId)
-                .orderByDesc(AlarmAudit::getAuditTime));
-        return audits.stream().map(this::toAuditOutDTO).collect(Collectors.toList());
-    }
-
-    @Override
-    public AlarmFlowOutDTO getAlarmFlow(String alarmId) {
-        AlarmFlowOutDTO flow = new AlarmFlowOutDTO();
-        flow.setAlarm(getAlarmDetail(alarmId));
-        flow.setAuditList(getAuditList(alarmId));
-        AlarmData alarm = alarmDataService.getById(alarmId);
-        WorkOrder order = alarm == null || alarm.getOrderId() == null
-                ? null : workOrderMapper.selectById(alarm.getOrderId());
-        // 出参统一用 WorkOrderVO,实体保持纯表映射
-        flow.setWorkOrder(WorkOrderVO.from(order));
-        if (order != null) {
-            EquipmentBase device = order.getDeviceId() == null ? null
-                    : equipmentBaseMapper.selectById(order.getDeviceId());
-            flow.getWorkOrder().setDeviceCode(device == null ? null : device.getEquipmentCode());
-            List<WorkOrderLog> logs = workOrderLogMapper.selectList(new LambdaQueryWrapper<WorkOrderLog>()
-                    .eq(WorkOrderLog::getOrderId, order.getOrderId())
-                    .orderByAsc(WorkOrderLog::getOperTime));
-            flow.setWorkOrderLogList(toLogVOList(logs));
-        }
-        return flow;
-    }
-
-    private AlarmAuditOutDTO toAuditOutDTO(AlarmAudit audit) {
-        AlarmAuditOutDTO vo = BeanUtil.copyProperties(audit, AlarmAuditOutDTO.class);
-        vo.setAuditActionText(audit.getAuditAction() != null && audit.getAuditAction() == 2 ? "解除" : "审核");
-        vo.setAuditResultText(audit.getAuditResult() != null && audit.getAuditResult() == 2 ? "误报" : "属实");
-        return vo;
-    }
-
-    /**
-     * 操作日志实体转出参:展示字段(操作类型描述、附件列表)回填到出参,实体保持纯表映射
-     */
-    private List<WorkOrderLogVO> toLogVOList(List<WorkOrderLog> logs) {
-        if (CollUtil.isEmpty(logs)) {
-            return new ArrayList<>();
-        }
-        List<WorkOrderLogVO> result = new ArrayList<>(logs.size());
-        for (WorkOrderLog log : logs) {
-            WorkOrderLogVO vo = WorkOrderLogVO.from(log);
-            vo.setOperTypeDesc(WorkOrderLogOperTypeEnum.getDescByCode(log.getOperType()));
-            vo.setAttachList(splitAttach(log.getAttachUrl()));
-            result.add(vo);
-        }
-        return result;
-    }
-
-    /**
-     * 操作附件 URL 拆分为列表,空串与空白项丢弃
-     */
-    private List<String> splitAttach(String attachUrl) {
-        if (StrUtil.isBlank(attachUrl)) {
-            return new ArrayList<>();
-        }
-        return StrUtil.split(attachUrl, ',').stream()
-                .map(String::trim)
-                .filter(StrUtil::isNotEmpty)
-                .collect(Collectors.toList());
+        AlarmLevelEnum level = AlarmLevelEnum.fromCode(alarmLevel);
+        return level == null ? "未知等级" : level.getLabel();
     }
 
     // ------------------------------------------------------------------
@@ -1038,24 +608,5 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     // 当前用户
     // ------------------------------------------------------------------
 
-    private Long currentUserId() {
-        LoginUser loginUser = SecurityUtils.getLoginUser();
-        return loginUser != null ? loginUser.getUserId() : null;
-    }
 
-    private Long currentDeptId() {
-        LoginUser loginUser = SecurityUtils.getLoginUser();
-        return loginUser != null ? loginUser.getDeptId() : null;
-    }
-
-    private String currentUserName() {
-        LoginUser loginUser = SecurityUtils.getLoginUser();
-        if (loginUser == null) {
-            return "系统";
-        }
-        if (loginUser.getUser() != null && StrUtil.isNotBlank(loginUser.getUser().getNickName())) {
-            return loginUser.getUser().getNickName();
-        }
-        return loginUser.getUsername();
-    }
 }

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

@@ -118,7 +118,7 @@ public class AlarmData implements Serializable {
     @TableField(exist = false)
     private Long workOrderId;
 
-    /** 审核状态:1待审核,2已审核(由报警和工单状态综合计算) */
-    @TableField(exist = false)
+    @ApiModelProperty("审核结果,见 AlarmAuditStatusEnum:0-未审核 1-确认报警 2-误报警")
+    @TableField(value = "audit_status")
     private Integer auditStatus;
 }

+ 34 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/AlarmLog.java

@@ -0,0 +1,34 @@
+package com.zksy.base.alarm.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+/** 报警操作流水,字段语义与 work_order_log 对齐。 */
+@Data
+@TableName(value = "alarm_log", schema = "app_user")
+public class AlarmLog {
+    @TableId(value = "log_id", type = IdType.AUTO)
+    @ApiModelProperty("日志ID,Kingbase int8,由 alarm_log_id_seq 序列自增")
+    private Long logId;
+    @ApiModelProperty("报警ID")
+    private String alarmId;
+    @ApiModelProperty("操作类型,见 AlarmLogOperTypeEnum")
+    private Integer operType;
+    @ApiModelProperty("操作说明 / 审核意见")
+    private String operDesc;
+    @ApiModelProperty("附件地址,多个用逗号分隔")
+    private String attachUrl;
+    @ApiModelProperty("操作人ID")
+    private Long operUserId;
+    @ApiModelProperty("操作人姓名")
+    private String operUserName;
+    @ApiModelProperty("操作时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime operTime;
+    @TableField(exist = false)
+    @ApiModelProperty("操作类型名称")
+    private String operTypeName;
+}

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

@@ -0,0 +1,31 @@
+package com.zksy.base.alarm.dto;
+
+import lombok.Data;
+import io.swagger.annotations.ApiModelProperty;
+import org.springframework.format.annotation.DateTimeFormat;
+import javax.validation.constraints.*;
+import java.time.LocalDateTime;
+
+@Data
+public class AlarmReviewQuery {
+    @NotNull(message = "设备大类不能为空")
+    @ApiModelProperty("equipment_type.id,设备大类根ID")
+    private Integer rootTypeId;
+    @ApiModelProperty("报警编码,精确匹配;燃气菜单限定 WARN-GAS-CONCENTRATION")
+    @Size(max = 100)
+    private String warningCode;
+    @Size(max = 100)
+    private String keyword;
+    private Integer alarmLevel;
+    private Integer auditStatus;
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss[.SSSSSS]")
+    private LocalDateTime startTime;
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss[.SSSSSS]")
+    private LocalDateTime endTime;
+    @Min(1)
+    private long pageNum = 1;
+    @Min(1) @Max(100)
+    private long pageSize = 10;
+    /** 只允许报警时间排序,不能传入任意 SQL 列。 */
+    private boolean ascending;
+}

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

@@ -0,0 +1,16 @@
+package com.zksy.base.alarm.dto;
+
+import lombok.Data;
+import javax.validation.constraints.*;
+
+@Data
+public class AlarmReviewRequest {
+    @NotNull(message = "请选择审核结果")
+    private Integer auditStatus;
+    @NotBlank(message = "请填写审核意见")
+    @Size(max = 500, message = "审核意见最多500字")
+    private String opinion;
+    /** 服务端生成的附件票据,避免任意拼接外部附件 URL。 */
+    @Size(max = 5, message = "最多上传5个附件")
+    private java.util.List<String> attachmentTokens;
+}

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

@@ -0,0 +1,15 @@
+package com.zksy.base.alarm.dto;
+
+import com.zksy.base.alarm.domain.AlarmData;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AlarmReviewVO extends AlarmData {
+    private String equipmentId;
+    private String equipmentLocation;
+    private String warningUnit;
+    private String alarmLevelName;
+    private String auditStatusName;
+}

+ 19 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmAuditStatusEnum.java

@@ -0,0 +1,19 @@
+package com.zksy.base.alarm.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import java.util.Arrays;
+
+/** 报警审核结果。 */
+@Getter
+@AllArgsConstructor
+public enum AlarmAuditStatusEnum {
+    UNREVIEWED(0, "未审核"), CONFIRMED(1, "确认报警"), FALSE_ALARM(2, "误报警");
+
+    private final int code;
+    private final String label;
+
+    public static AlarmAuditStatusEnum fromCode(Integer code) {
+        return Arrays.stream(values()).filter(value -> code != null && value.code == code).findFirst().orElse(null);
+    }
+}

+ 17 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmDisplayStatusEnum.java

@@ -0,0 +1,17 @@
+package com.zksy.base.alarm.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import java.util.Arrays;
+
+/** 报警台账展示状态,不替代设备状态或审核结果。 */
+@Getter
+@AllArgsConstructor
+public enum AlarmDisplayStatusEnum {
+    PENDING(1, "待审核"), TRANSFERRED(2, "已转工单"), RELEASED(3, "已解除"), FALSE_ALARM(4, "误报警");
+    private final int code;
+    private final String label;
+    public static AlarmDisplayStatusEnum fromCode(Integer code) {
+        return Arrays.stream(values()).filter(value -> code != null && value.code == code).findFirst().orElse(null);
+    }
+}

+ 19 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmLevelEnum.java

@@ -0,0 +1,19 @@
+package com.zksy.base.alarm.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import java.util.Arrays;
+
+/** 报警等级。 */
+@Getter
+@AllArgsConstructor
+public enum AlarmLevelEnum {
+    LEVEL_ONE(1, "一级报警"), LEVEL_TWO(2, "二级报警"), LEVEL_THREE(3, "三级报警"), LEVEL_FOUR(4, "四级报警");
+
+    private final int code;
+    private final String label;
+
+    public static AlarmLevelEnum fromCode(Integer code) {
+        return Arrays.stream(values()).filter(value -> code != null && value.code == code).findFirst().orElse(null);
+    }
+}

+ 19 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmLogOperTypeEnum.java

@@ -0,0 +1,19 @@
+package com.zksy.base.alarm.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import java.util.Arrays;
+
+/** 报警日志操作类型。 */
+@Getter
+@AllArgsConstructor
+public enum AlarmLogOperTypeEnum {
+    CREATED(0, "报警触发"), CONFIRMED(1, "确认报警"), FALSE_ALARM(2, "误报警"), HANDLED(3, "报警处理"), ORDER_LINKED(4, "关联工单"), WORK_ORDER_OPERATION(5, "工单操作");
+
+    private final int code;
+    private final String label;
+
+    public static AlarmLogOperTypeEnum fromCode(Integer code) {
+        return Arrays.stream(values()).filter(value -> code != null && value.code == code).findFirst().orElse(null);
+    }
+}

+ 19 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/enums/AlarmRecordStatusEnum.java

@@ -0,0 +1,19 @@
+package com.zksy.base.alarm.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import java.util.Arrays;
+
+/** 报警处理状态(与设备报警状态不同)。 */
+@Getter
+@AllArgsConstructor
+public enum AlarmRecordStatusEnum {
+    UNHANDLED(0, "未处理"), HANDLED(1, "已处理");
+
+    private final int code;
+    private final String label;
+
+    public static AlarmRecordStatusEnum fromCode(Integer code) {
+        return Arrays.stream(values()).filter(value -> code != null && value.code == code).findFirst().orElse(null);
+    }
+}

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

@@ -0,0 +1,9 @@
+package com.zksy.base.alarm.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.base.alarm.domain.AlarmLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AlarmLogMapper extends BaseMapper<AlarmLog> {
+}

+ 18 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/mapper/AlarmReviewMapper.java

@@ -0,0 +1,18 @@
+package com.zksy.base.alarm.mapper;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.alarm.dto.*;
+import org.apache.ibatis.annotations.*;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface AlarmReviewMapper {
+    Page<AlarmReviewVO> selectPage(Page<AlarmReviewVO> page, @Param("query") AlarmReviewQuery query,
+                                  @Param("typeIds") List<Integer> typeIds, @Param("unhandled") int unhandled);
+    AlarmReviewVO selectDetail(@Param("id") String id, @Param("query") AlarmReviewQuery query,
+                              @Param("typeIds") List<Integer> typeIds);
+    Map<String, Long> selectStatistics(@Param("query") AlarmReviewQuery query,
+                                      @Param("typeIds") List<Integer> typeIds,
+                                      @Param("unhandled") int unhandled, @Param("unreviewed") int unreviewed);
+}

+ 57 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewAttachmentService.java

@@ -0,0 +1,57 @@
+package com.zksy.base.alarm.service;
+
+import com.zksy.common.config.ZksyConfig;
+import com.zksy.common.core.redis.RedisCache;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.common.utils.file.FileUploadUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.InputStream;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/** 复用框架上传目录;票据绑定操作人和报警,审核接口不能伪造附件地址。 */
+@Service
+@RequiredArgsConstructor
+public class AlarmReviewAttachmentService {
+    private final RedisCache redis;
+    private static final long MAX_BYTES = 10L * 1024 * 1024;
+
+    public Map<String, String> upload(String alarmId, MultipartFile file) throws Exception {
+        if (file == null || file.isEmpty() || file.getSize() > MAX_BYTES)
+            throw new ServiceException("附件不能为空且单个不得超过10MB");
+        String name = Objects.toString(file.getOriginalFilename(), "").toLowerCase(Locale.ROOT);
+        byte[] header;
+        try (InputStream input = file.getInputStream()) { header = input.readNBytes(8); }
+        boolean jpeg = (name.endsWith(".jpg") || name.endsWith(".jpeg")) && header.length >= 3
+                && (header[0] & 255) == 255 && (header[1] & 255) == 216 && (header[2] & 255) == 255;
+        boolean png = name.endsWith(".png") && Arrays.equals(header, new byte[]{(byte)137,80,78,71,13,10,26,10});
+        if (!jpeg && !png) throw new ServiceException("仅支持内容与扩展名一致的JPG、JPEG、PNG图片");
+        String path = FileUploadUtils.upload(ZksyConfig.getUploadPath(), file, new String[]{"jpg", "jpeg", "png"}, true);
+        String token = UUID.randomUUID().toString();
+        redis.setCacheObject(key(alarmId, token), path, 2, TimeUnit.HOURS);
+        Map<String, String> result = new LinkedHashMap<>();
+        result.put("token", token); result.put("url", path); result.put("name", file.getOriginalFilename());
+        return result;
+    }
+
+    public String resolve(String alarmId, List<String> tokens) {
+        if (tokens == null || tokens.isEmpty()) return null;
+        if (tokens.size() > 5 || new HashSet<>(tokens).size() != tokens.size())
+            throw new ServiceException("最多上传5个不重复的附件");
+        List<String> paths = new ArrayList<>();
+        for (String token : tokens) {
+            if (token == null || !token.matches("[a-fA-F0-9-]{36}")) throw new ServiceException("附件凭据无效");
+            String path = redis.getCacheObject(key(alarmId, token));
+            if (path == null) throw new ServiceException("附件已过期或不属于本次审核,请重新上传");
+            paths.add(path);
+        }
+        return String.join(",", paths);
+    }
+
+    private String key(String alarmId, String token) {
+        return "alarm:review:attachment:" + SecurityUtils.getUserId() + ":" + alarmId + ":" + token;
+    }
+}

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

@@ -0,0 +1,58 @@
+package com.zksy.base.alarm.service;
+
+import cn.hutool.json.JSONUtil;
+import cn.hutool.json.JSONObject;
+import com.zksy.base.alarm.dto.AlarmReviewQuery;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.enums.EquipmentTypeEnum;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+import com.zksy.common.core.domain.entity.SysMenu;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.system.service.ISysMenuService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.security.access.AccessDeniedException;
+import java.util.*;
+
+/** 路由 query 不是授权依据,服务端同时校验用户获授权菜单及数据库设备类型树。 */
+@Service
+@RequiredArgsConstructor
+public class AlarmReviewScopeService {
+    private final EquipmentTypeMapper typeMapper;
+    private final ISysMenuService menuService;
+
+    public List<Integer> resolve(AlarmReviewQuery query) {
+        EquipmentTypeEnum root = EquipmentTypeEnum.getById(query.getRootTypeId());
+        if (root == null || !root.isRoot()) throw new ServiceException("无效的设备大类");
+        if (!SecurityUtils.isAdmin(SecurityUtils.getUserId())) {
+            boolean allowed = menuService.selectMenuList(SecurityUtils.getUserId()).stream()
+                    .anyMatch(menu -> matches(menu, query));
+            if (!allowed) throw new AccessDeniedException("无权访问当前子系统的报警数据");
+        }
+        List<EquipmentType> types = typeMapper.selectList(null);
+        Set<Integer> ids = new LinkedHashSet<>();
+        if (types.stream().noneMatch(type -> root.getId().equals(type.getId()))) return new ArrayList<>();
+        ids.add(root.getId());
+        boolean changed;
+        do {
+            changed = false;
+            for (EquipmentType type : types) {
+                if (type.getId() != null && ids.contains(type.getParentId())) changed |= ids.add(type.getId());
+            }
+        } while (changed);
+        return new ArrayList<>(ids);
+    }
+
+    boolean matches(SysMenu menu, AlarmReviewQuery query) {
+        if (!"subSystem/alarm/index".equals(menu.getComponent()) || !"0".equals(menu.getStatus())) return false;
+        try {
+            JSONObject scope = JSONUtil.parseObj(menu.getQuery());
+            String code = scope.getStr("warning_code", scope.getStr("warningCode"));
+            return Objects.equals(scope.getInt("rootTypeId"), query.getRootTypeId())
+                    && (code == null || code.isEmpty() || Objects.equals(code, query.getWarningCode()));
+        } catch (RuntimeException invalidMenu) {
+            return false;
+        }
+    }
+}

+ 135 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewService.java

@@ -0,0 +1,135 @@
+package com.zksy.base.alarm.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.alarm.domain.*;
+import com.zksy.base.alarm.dto.*;
+import com.zksy.base.alarm.enums.*;
+import com.zksy.base.alarm.mapper.*;
+import com.zksy.base.enums.EquipmentTypeEnum;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.ops.service.WorkOrderService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AlarmReviewService {
+    private final AlarmReviewMapper reviewMapper;
+    private final AlarmDataMapper alarmMapper;
+    private final AlarmLogMapper logMapper;
+    private final AlarmReviewScopeService scopeService;
+    private final AlarmReviewAttachmentService attachments;
+    private final WorkOrderService workOrderService;
+
+    public Page<AlarmReviewVO> page(AlarmReviewQuery query) {
+        validate(query);
+        Page<AlarmReviewVO> page = reviewMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()),
+                query, scopeService.resolve(query), AlarmRecordStatusEnum.UNHANDLED.getCode());
+        page.getRecords().forEach(this::labels);
+        return page;
+    }
+
+    public Map<String, Long> statistics(AlarmReviewQuery query) {
+        validate(query);
+        return reviewMapper.selectStatistics(query, scopeService.resolve(query),
+                AlarmRecordStatusEnum.UNHANDLED.getCode(), AlarmAuditStatusEnum.UNREVIEWED.getCode());
+    }
+
+    public AlarmReviewVO detail(String id, AlarmReviewQuery query) {
+        AlarmReviewVO alarm = reviewMapper.selectDetail(id, query, scopeService.resolve(query));
+        if (alarm == null) throw new ServiceException("报警不存在或不属于当前管理范围");
+        labels(alarm);
+        return alarm;
+    }
+
+    public List<AlarmLog> logs(String id, AlarmReviewQuery query) {
+        detail(id, query);
+        List<AlarmLog> logs = logMapper.selectList(new LambdaQueryWrapper<AlarmLog>()
+                .eq(AlarmLog::getAlarmId, id).orderByAsc(AlarmLog::getOperTime, AlarmLog::getLogId));
+        logs.forEach(log -> {
+            AlarmLogOperTypeEnum type = AlarmLogOperTypeEnum.fromCode(log.getOperType());
+            log.setOperTypeName(type == null ? "历史操作" : type.getLabel());
+        });
+        return logs;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public AlarmReviewVO review(String id, AlarmReviewQuery query, AlarmReviewRequest request) {
+        AlarmReviewVO alarm = detail(id, query);
+        AlarmAuditStatusEnum result = AlarmAuditStatusEnum.fromCode(request.getAuditStatus());
+        if (result == null || result == AlarmAuditStatusEnum.UNREVIEWED) throw new ServiceException("审核结果无效");
+        String opinion = request.getOpinion() == null ? "" : request.getOpinion().trim();
+        if (opinion.isEmpty() || opinion.length() > 500) throw new ServiceException("审核意见须为1至500字");
+        String attachUrl = attachments.resolve(id, request.getAttachmentTokens());
+        LocalDateTime now = LocalDateTime.now();
+        // 原子条件更新锁定待审核报警;并发审核/监测恢复时,只有一方成功。
+        int updated = alarmMapper.update(null, new LambdaUpdateWrapper<AlarmData>()
+                .eq(AlarmData::getId, id)
+                .eq(AlarmData::getAlarmStatus, AlarmRecordStatusEnum.UNHANDLED.getCode())
+                .eq(AlarmData::getAuditStatus, AlarmAuditStatusEnum.UNREVIEWED.getCode())
+                .set(AlarmData::getAuditStatus, result.getCode()).set(AlarmData::getUpdateTime, now));
+        if (updated != 1) throw new ServiceException("报警已审核或已处理,请刷新后重试");
+        AlarmLog log = new AlarmLog();
+        log.setAlarmId(id);
+        log.setOperType(result == AlarmAuditStatusEnum.CONFIRMED
+                ? AlarmLogOperTypeEnum.CONFIRMED.getCode() : AlarmLogOperTypeEnum.FALSE_ALARM.getCode());
+        log.setOperDesc(opinion);
+        log.setAttachUrl(attachUrl);
+        log.setOperUserId(SecurityUtils.getUserId());
+        log.setOperUserName(SecurityUtils.getUsername());
+        log.setOperTime(now);
+        if (logMapper.insert(log) != 1) throw new ServiceException("审核日志保存失败");
+        if (result == AlarmAuditStatusEnum.CONFIRMED) {
+            // 工单故障描述由原始报警信息生成;审核意见仅写审核日志,不能覆盖故障事实。
+            Long orderId = workOrderService.createWorkOrderFromAlarm(id, null, null);
+            if (orderId == null) throw new ServiceException("工单创建失败");
+            alarm.setOrderId(orderId);
+        }
+        // 审核不会修改 equipment_status;误报仅记录结论,保留原始报警。
+        alarm.setAuditStatus(result.getCode());
+        labels(alarm);
+        return alarm;
+    }
+
+    public Map<String, Object> options() {
+        Map<String, Object> options = new LinkedHashMap<>();
+        options.put("auditStatuses", Arrays.stream(AlarmAuditStatusEnum.values())
+                .map(value -> option(value.name(), value.getCode(), value.getLabel())).collect(Collectors.toList()));
+        options.put("alarmLevels", Arrays.stream(AlarmLevelEnum.values())
+                .map(value -> option(value.name(), value.getCode(), value.getLabel())).collect(Collectors.toList()));
+        options.put("rootTypes", Arrays.stream(EquipmentTypeEnum.values()).filter(EquipmentTypeEnum::isRoot)
+                .map(value -> option(value.name(), value.getId(), value.getDesc())).collect(Collectors.toList()));
+        return options;
+    }
+
+    private Map<String, Object> option(String key, int value, String label) {
+        Map<String, Object> option = new LinkedHashMap<>();
+        option.put("key", key); option.put("value", value); option.put("label", label);
+        return option;
+    }
+
+    private void validate(AlarmReviewQuery query) {
+        if (query.getPageNum() < 1 || query.getPageSize() < 1 || query.getPageSize() > 100)
+            throw new ServiceException("分页参数无效");
+        if (query.getAlarmLevel() != null && AlarmLevelEnum.fromCode(query.getAlarmLevel()) == null)
+            throw new ServiceException("报警等级无效");
+        if (query.getAuditStatus() != null && AlarmAuditStatusEnum.fromCode(query.getAuditStatus()) == null)
+            throw new ServiceException("审核结果无效");
+        if (query.getStartTime() != null && query.getEndTime() != null && query.getStartTime().isAfter(query.getEndTime()))
+            throw new ServiceException("开始时间不能晚于结束时间");
+    }
+
+    private void labels(AlarmReviewVO alarm) {
+        AlarmLevelEnum level = AlarmLevelEnum.fromCode(alarm.getAlarmLevel());
+        AlarmAuditStatusEnum audit = AlarmAuditStatusEnum.fromCode(alarm.getAuditStatus());
+        alarm.setAlarmLevelName(level == null ? "未知等级" : level.getLabel());
+        alarm.setAuditStatusName(audit == null ? "未知结果" : audit.getLabel());
+    }
+}

+ 4 - 33
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmClearServiceImpl.java

@@ -3,9 +3,8 @@ package com.zksy.base.alarm.service.impl;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.collection.CollUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
-import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
 import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.enums.AlarmAuditStatusEnum;
 import com.zksy.base.alarm.service.AlarmClearResult;
 import com.zksy.base.alarm.service.AlarmClearService;
 import com.zksy.base.alarm.service.AlarmDataService;
@@ -26,7 +25,6 @@ import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -58,15 +56,9 @@ import java.util.stream.Collectors;
 @Service
 public class AlarmClearServiceImpl implements AlarmClearService {
 
-    /** 审核结论:2-误报 */
-    private static final int AUDIT_RESULT_FALSE_ALARM = 2;
-    /** 审核动作:2-误报解除 */
-    private static final int AUDIT_ACTION_FALSE_ALARM_RELEASE = 2;
-
     private static final int DEVICE_ALARM_STATUS_NORMAL = 0;
 
     private final AlarmDataService alarmDataService;
-    private final AlarmAuditMapper alarmAuditMapper;
     private final WorkOrderMapper workOrderMapper;
     private final EquipmentBaseMapper equipmentBaseMapper;
     private final EquipmentStatusMapper equipmentStatusMapper;
@@ -74,13 +66,11 @@ public class AlarmClearServiceImpl implements AlarmClearService {
 
     @Autowired
     public AlarmClearServiceImpl(AlarmDataService alarmDataService,
-                                 AlarmAuditMapper alarmAuditMapper,
                                  WorkOrderMapper workOrderMapper,
                                  EquipmentBaseMapper equipmentBaseMapper,
                                  EquipmentStatusMapper equipmentStatusMapper,
                                  WaterSupplyQueryService waterSupplyQueryService) {
         this.alarmDataService = alarmDataService;
-        this.alarmAuditMapper = alarmAuditMapper;
         this.workOrderMapper = workOrderMapper;
         this.equipmentBaseMapper = equipmentBaseMapper;
         this.equipmentStatusMapper = equipmentStatusMapper;
@@ -114,10 +104,9 @@ public class AlarmClearServiceImpl implements AlarmClearService {
         List<AlarmData> remaining = new ArrayList<>();
         List<AlarmData> resolved = new ArrayList<>();
         if (!alarms.isEmpty()) {
-            List<String> alarmIds = alarms.stream().map(AlarmData::getId)
-                    .filter(StrUtil::isNotBlank).collect(Collectors.toList());
-            Set<String> releasedByAudit = alarmIds.isEmpty() ? Collections.emptySet()
-                    : loadFalseAlarmReleasedIds(alarmIds);
+            Set<String> releasedByAudit = alarms.stream()
+                    .filter(alarm -> Objects.equals(alarm.getAuditStatus(), AlarmAuditStatusEnum.FALSE_ALARM.getCode()))
+                    .map(AlarmData::getId).collect(Collectors.toSet());
             Map<String, List<WorkOrder>> ordersByAlarm = loadOrdersByAlarmId(alarms);
             for (AlarmData alarm : alarms) {
                 if (isCauseResolved(alarm, releasedByAudit, ordersByAlarm)) {
@@ -187,24 +176,6 @@ public class AlarmClearServiceImpl implements AlarmClearService {
         return Integer.valueOf(1).equals(alarm.getAlarmStatus());
     }
 
-    private Set<String> loadFalseAlarmReleasedIds(List<String> alarmIds) {
-        List<AlarmAudit> audits = alarmAuditMapper.selectList(new LambdaQueryWrapper<AlarmAudit>()
-                .in(AlarmAudit::getAlarmId, alarmIds)
-                .orderByDesc(AlarmAudit::getAuditId));
-        Map<String, AlarmAudit> latest = new LinkedHashMap<>();
-        for (AlarmAudit audit : audits) {
-            latest.putIfAbsent(audit.getAlarmId(), audit);
-        }
-        Set<String> released = new LinkedHashSet<>();
-        latest.forEach((alarmId, audit) -> {
-            if (Objects.equals(audit.getAuditResult(), AUDIT_RESULT_FALSE_ALARM)
-                    || Objects.equals(audit.getAuditAction(), AUDIT_ACTION_FALSE_ALARM_RELEASE)) {
-                released.add(alarmId);
-            }
-        });
-        return released;
-    }
-
     /** 报警ID -> 其全部关联工单:闭环判定需要同时看到未办结、已结案与已取消。 */
     private Map<String, List<WorkOrder>> loadOrdersByAlarmId(List<AlarmData> alarms) {
         List<Long> orderIds = alarms.stream().map(AlarmData::getOrderId)

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

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.enums.AlarmAuditStatusEnum;
+import com.zksy.base.alarm.enums.AlarmRecordStatusEnum;
 import com.zksy.base.alarm.mapper.AlarmDataMapper;
 import com.zksy.base.alarm.service.AlarmDataService;
 import com.zksy.base.domain.EquipmentBase;
@@ -59,7 +61,7 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
 
         // 填充 deviceName/longitude/latitude:通过 device_code 从 equipment_base 表关联查询
         fillDeviceInfo(result.getRecords());
-        // 报警可能已经创建工单但 alarm_status 未同步,列表状态以工单关联为准补齐。
+        // 只回填工单关联信息,审核状态与报警处理状态均以持久字段为准。
         fillWorkOrderStatus(result.getRecords());
 
         return result;
@@ -141,7 +143,6 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
             boolean hasOrder = order != null;
             record.setHasWorkOrder(hasOrder);
             record.setWorkOrderId(hasOrder ? order.getOrderId() : null);
-            record.setAuditStatus(hasOrder || Integer.valueOf(1).equals(record.getAlarmStatus()) ? 2 : 1);
         }
     }
 
@@ -235,7 +236,10 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
             alarmData.setCreateTime(LocalDateTime.now());
         }
         if (alarmData.getAlarmStatus() == null) {
-            alarmData.setAlarmStatus(0);
+            alarmData.setAlarmStatus(AlarmRecordStatusEnum.UNHANDLED.getCode());
+        }
+        if (alarmData.getAuditStatus() == null) {
+            alarmData.setAuditStatus(AlarmAuditStatusEnum.UNREVIEWED.getCode());
         }
         return this.save(alarmData);
     }

+ 9 - 62
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillAlarmSyncService.java

@@ -1,19 +1,14 @@
 package com.zksy.base.drill.service;
 
-import cn.hutool.core.date.DateUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.mapper.AlarmDataMapper;
 import com.zksy.base.domain.CaseInfo;
+import com.zksy.base.enums.EquipmentTypeEnum;
+import com.zksy.base.alarm.enums.AlarmLevelEnum;
 import com.zksy.base.domain.EquipmentBase;
-import com.zksy.ops.enums.WorkOrderLogOperTypeEnum;
-import com.zksy.ops.enums.WorkOrderStatusEnum;
-import com.zksy.ops.domain.WorkOrder;
-import com.zksy.ops.domain.WorkOrderLog;
 import com.zksy.base.mapper.CaseInfoMapper;
 import com.zksy.base.mapper.EquipmentBaseMapper;
-import com.zksy.ops.mapper.WorkOrderLogMapper;
-import com.zksy.ops.mapper.WorkOrderMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -21,12 +16,10 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Random;
 
 /**
- * 演练告警同步创建案件/工单 — 从 zk-api-service 的 AlarmDataServiceImpl.syncCreateCaseAndWorkOrder 复制
+ * 演练报警只创建必要案件;工单由统一报警审核确认后创建。
  */
 @Slf4j
 @Service("drillAlarmSyncServiceDrill")
@@ -38,27 +31,17 @@ public class DrillAlarmSyncService {
     @Autowired
     private CaseInfoMapper caseInfoMapper;
 
-    @Autowired
-    private WorkOrderMapper workOrderMapper;
-
-    @Autowired
-    private WorkOrderLogMapper workOrderLogMapper;
-
     @Autowired
     private AlarmDataMapper alarmDataMapper;
 
-    private static final List<Integer> CASE_REQUIRED_TYPE_IDS = Arrays.asList(5, 9, 10);
-
     @Transactional(rollbackFor = Exception.class)
-    public void syncCreateCaseAndWorkOrder(AlarmData alarmData) {
+    public void syncCreateCase(AlarmData alarmData) {
         AlarmData stored = alarmDataMapper.selectById(alarmData.getId());
         if (stored == null) {
-            throw new IllegalArgumentException("报警记录不存在,无法关联工单");
-        }
-        if (stored.getOrderId() != null) {
-            alarmData.setOrderId(stored.getOrderId());
-            return;
+            throw new IllegalArgumentException("报警记录不存在,无法关联案件");
         }
+        if (caseInfoMapper.selectCount(new LambdaQueryWrapper<CaseInfo>()
+                .eq(CaseInfo::getAlarmId, alarmData.getId())) > 0) return;
         EquipmentBase equipmentBase = equipmentBaseMapper.selectOne(
                 new LambdaQueryWrapper<EquipmentBase>()
                         .eq(EquipmentBase::getEquipmentCode, alarmData.getDeviceCode())
@@ -68,9 +51,8 @@ public class DrillAlarmSyncService {
             return;
         }
         Integer typeId = equipmentBase.getEquipmentTypeId();
-        boolean needCreateCase = CASE_REQUIRED_TYPE_IDS.contains(typeId);
-        int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : 4;
-        int orderLevel = caseLevel <= 2 ? 0 : (caseLevel == 3 ? 1 : 2);
+        boolean needCreateCase = EquipmentTypeEnum.MANHOLE_COVER_MONITOR.getId().equals(typeId);
+        int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : AlarmLevelEnum.LEVEL_FOUR.getCode();
         String desc = alarmData.getWarningType() != null ? alarmData.getWarningType() : "设备告警";
         if (alarmData.getRemark() != null) {
             desc += "-" + alarmData.getRemark();
@@ -88,37 +70,6 @@ public class DrillAlarmSyncService {
             caseInfoMapper.insert(caseInfo);
             log.info("同步新增案件信息成功,caseNo={}", caseInfo.getCaseNo());
         }
-        WorkOrder workOrder = new WorkOrder();
-        workOrder.setOrderNo(generateOrderNo());
-        workOrder.setDeviceId(equipmentBase.getEquipmentId());
-        workOrder.setOrderLevel(orderLevel);
-        workOrder.setOrderDesc(desc);
-        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
-        workOrderMapper.insert(workOrder);
-        alarmData.setOrderId(workOrder.getOrderId());
-        AlarmData relation = new AlarmData();
-        relation.setId(alarmData.getId());
-        relation.setOrderId(workOrder.getOrderId());
-        if (alarmDataMapper.update(relation, new LambdaQueryWrapper<AlarmData>()
-                .eq(AlarmData::getId, alarmData.getId()).isNull(AlarmData::getOrderId)) != 1) {
-            throw new IllegalStateException("报警关联工单失败");
-        }
-        if (caseInfo != null) {
-            caseInfo.setOrderId(workOrder.getOrderId());
-            if (caseInfoMapper.updateById(caseInfo) != 1) {
-                throw new IllegalStateException("案件关联工单失败");
-            }
-        }
-        log.info("同步新增工单成功,orderNo={}", workOrder.getOrderNo());
-        WorkOrderLog workOrderLog = new WorkOrderLog();
-        workOrderLog.setOrderId(workOrder.getOrderId());
-        workOrderLog.setOperType(WorkOrderLogOperTypeEnum.CREATE.getCode());
-        workOrderLog.setOperDesc(desc + ",系统告警自动创建工单");
-        workOrderLog.setOperUserName("系统");
-        workOrderLog.setOperUserId(-1L);
-        workOrderLog.setOperTime(DateUtil.date());
-        workOrderLogMapper.insert(workOrderLog);
-        log.info("同步新增工单操作日志成功,orderId={}", workOrder.getOrderId());
     }
 
     private static String generateCaseNo() {
@@ -126,8 +77,4 @@ public class DrillAlarmSyncService {
         return "CS" + dt + String.format("%04d", new Random().nextInt(10000));
     }
 
-    private static String generateOrderNo() {
-        String dt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
-        return "WO" + dt + String.format("%04d", new Random().nextInt(10000));
-    }
 }

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/impl/DrillSimulationServiceImpl.java

@@ -73,7 +73,7 @@ public class DrillSimulationServiceImpl implements DrillSimulationService {
                 alarm.setCreateTime(now);
                 boolean saved = alarmDataService.saveAlarmData(alarm);
                 if (saved) {
-                    drillAlarmSyncService.syncCreateCaseAndWorkOrder(alarm);
+                    drillAlarmSyncService.syncCreateCase(alarm);
                     successCount++;
                 } else {
                     failCount++;

+ 8 - 60
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/DrillAlarmSyncService.java

@@ -1,19 +1,14 @@
 package com.zksy.base.service;
 
-import cn.hutool.core.date.DateUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.mapper.AlarmDataMapper;
 import com.zksy.base.domain.CaseInfo;
+import com.zksy.base.enums.EquipmentTypeEnum;
+import com.zksy.base.alarm.enums.AlarmLevelEnum;
 import com.zksy.base.domain.EquipmentBase;
-import com.zksy.ops.enums.WorkOrderLogOperTypeEnum;
-import com.zksy.ops.enums.WorkOrderStatusEnum;
-import com.zksy.ops.domain.WorkOrder;
-import com.zksy.ops.domain.WorkOrderLog;
 import com.zksy.base.mapper.CaseInfoMapper;
 import com.zksy.base.mapper.EquipmentBaseMapper;
-import com.zksy.ops.mapper.WorkOrderLogMapper;
-import com.zksy.ops.mapper.WorkOrderMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -21,8 +16,6 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Random;
 
 @Slf4j
@@ -34,24 +27,16 @@ public class DrillAlarmSyncService {
     @Autowired
     private CaseInfoMapper caseInfoMapper;
     @Autowired
-    private WorkOrderMapper workOrderMapper;
-    @Autowired
-    private WorkOrderLogMapper workOrderLogMapper;
-    @Autowired
     private AlarmDataMapper alarmDataMapper;
 
-    private static final List<Integer> CASE_REQUIRED_TYPE_IDS = Arrays.asList(5, 9, 10);
-
     @Transactional(rollbackFor = Exception.class)
-    public void syncCreateCaseAndWorkOrder(AlarmData alarmData) {
+    public void syncCreateCase(AlarmData alarmData) {
         AlarmData stored = alarmDataMapper.selectById(alarmData.getId());
         if (stored == null) {
-            throw new IllegalArgumentException("报警记录不存在,无法关联工单");
-        }
-        if (stored.getOrderId() != null) {
-            alarmData.setOrderId(stored.getOrderId());
-            return;
+            throw new IllegalArgumentException("报警记录不存在,无法关联案件");
         }
+        if (caseInfoMapper.selectCount(new LambdaQueryWrapper<CaseInfo>()
+                .eq(CaseInfo::getAlarmId, alarmData.getId())) > 0) return;
         EquipmentBase equipmentBase = equipmentBaseMapper.selectOne(
                 new LambdaQueryWrapper<EquipmentBase>()
                         .eq(EquipmentBase::getEquipmentCode, alarmData.getDeviceCode())
@@ -61,9 +46,8 @@ public class DrillAlarmSyncService {
             return;
         }
         Integer typeId = equipmentBase.getEquipmentTypeId();
-        boolean needCreateCase = CASE_REQUIRED_TYPE_IDS.contains(typeId);
-        int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : 4;
-        int orderLevel = caseLevel <= 2 ? 0 : (caseLevel == 3 ? 1 : 2);
+        boolean needCreateCase = EquipmentTypeEnum.MANHOLE_COVER_MONITOR.getId().equals(typeId);
+        int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : AlarmLevelEnum.LEVEL_FOUR.getCode();
         String desc = alarmData.getWarningType() != null ? alarmData.getWarningType() : "设备告警";
         if (alarmData.getRemark() != null) {
             desc += "-" + alarmData.getRemark();
@@ -81,38 +65,6 @@ public class DrillAlarmSyncService {
             caseInfoMapper.insert(caseInfo);
             log.info("同步新增案件信息成功,caseNo={}", caseInfo.getCaseNo());
         }
-        WorkOrder workOrder = new WorkOrder();
-        workOrder.setOrderNo(generateOrderNo());
-        workOrder.setDeviceId(equipmentBase.getEquipmentId());
-        workOrder.setOrderLevel(orderLevel);
-        workOrder.setOrderDesc(desc);
-        workOrder.setCreateTime(DateUtil.date());
-        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
-        workOrderMapper.insert(workOrder);
-        alarmData.setOrderId(workOrder.getOrderId());
-        AlarmData relation = new AlarmData();
-        relation.setId(alarmData.getId());
-        relation.setOrderId(workOrder.getOrderId());
-        if (alarmDataMapper.update(relation, new LambdaQueryWrapper<AlarmData>()
-                .eq(AlarmData::getId, alarmData.getId()).isNull(AlarmData::getOrderId)) != 1) {
-            throw new IllegalStateException("报警关联工单失败");
-        }
-        if (caseInfo != null) {
-            caseInfo.setOrderId(workOrder.getOrderId());
-            if (caseInfoMapper.updateById(caseInfo) != 1) {
-                throw new IllegalStateException("案件关联工单失败");
-            }
-        }
-        log.info("同步新增工单成功,orderNo={}", workOrder.getOrderNo());
-        WorkOrderLog workOrderLog = new WorkOrderLog();
-        workOrderLog.setOrderId(workOrder.getOrderId());
-        workOrderLog.setOperType(WorkOrderLogOperTypeEnum.CREATE.getCode());
-        workOrderLog.setOperDesc(desc + ",系统告警自动创建工单");
-        workOrderLog.setOperUserName("系统");
-        workOrderLog.setOperUserId(-1L);
-        workOrderLog.setOperTime(DateUtil.date());
-        workOrderLogMapper.insert(workOrderLog);
-        log.info("同步新增工单操作日志成功,orderId={}", workOrder.getOrderId());
     }
 
     private static String generateCaseNo() {
@@ -120,8 +72,4 @@ public class DrillAlarmSyncService {
         return "CS" + dt + String.format("%04d", new Random().nextInt(10000));
     }
 
-    private static String generateOrderNo() {
-        String dt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
-        return "WO" + dt + String.format("%04d", new Random().nextInt(10000));
-    }
 }

+ 0 - 16
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/GasMonitorService.java

@@ -1,16 +0,0 @@
-package com.zksy.base.service;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * 燃气监测页面 — 场站可燃气体浓度监测
- */
-public interface GasMonitorService {
-
-    /** 监测点列表(含最新气体浓度+温度) */
-    List<Map<String, Object>> getPoints();
-
-    /** 24小时气体浓度趋势 */
-    Map<String, Object> getTrend(String pointId, int hours);
-}

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/DrillSimulationServiceImpl.java

@@ -73,7 +73,7 @@ public class DrillSimulationServiceImpl implements DrillSimulationService {
                 alarm.setCreateTime(now);
                 boolean saved = alarmDataService.saveAlarmData(alarm);
                 if (saved) {
-                    drillAlarmSyncService.syncCreateCaseAndWorkOrder(alarm);
+                    drillAlarmSyncService.syncCreateCase(alarm);
                     successCount++;
                 } else {
                     failCount++;

+ 0 - 233
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/GasMonitorServiceImpl.java

@@ -1,233 +0,0 @@
-package com.zksy.base.service.impl;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.zksy.base.domain.EquipmentBase;
-import com.zksy.base.domain.EquipmentPointRel;
-import com.zksy.base.domain.EquipmentType;
-import com.zksy.base.domain.MonitorPoint;
-import com.zksy.base.environment.domain.ERealTimeData;
-import com.zksy.base.environment.mapper.ERealTimeDataMapper;
-import com.zksy.base.enums.EquipmentTypeEnum;
-import com.zksy.base.gas.domain.GasMonitorData;
-import com.zksy.base.gas.mapper.GasMonitorDataMapper;
-import com.zksy.base.mapper.EquipmentBaseMapper;
-import com.zksy.base.mapper.EquipmentPointRelMapper;
-import com.zksy.base.mapper.EquipmentTypeMapper;
-import com.zksy.base.mapper.MonitorPointMapper;
-import com.zksy.base.service.GasMonitorService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.math.RoundingMode;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.*;
-import java.util.stream.Collectors;
-
-@Slf4j
-@Service
-public class GasMonitorServiceImpl implements GasMonitorService {
-
-    @Autowired
-    private MonitorPointMapper monitorPointMapper;
-    @Autowired
-    private EquipmentPointRelMapper equipmentPointRelMapper;
-    @Autowired
-    private EquipmentBaseMapper equipmentBaseMapper;
-    @Autowired
-    private GasMonitorDataMapper gasMonitorDataMapper;
-    @Autowired
-    private ERealTimeDataMapper eRealTimeDataMapper;
-    @Autowired
-    private EquipmentTypeMapper equipmentTypeMapper;
-
-    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-
-    /** 燃气设备父类ID(整数主键,取值见 EquipmentTypeEnum) */
-    private static final Integer GAS_parent_id = EquipmentTypeEnum.ROOT_GAS_DEVICE.getId();
-
-    @Override
-    public List<Map<String, Object>> getPoints() {
-        List<MonitorPoint> points = monitorPointMapper.selectList(
-                new LambdaQueryWrapper<MonitorPoint>()
-                        .eq(MonitorPoint::getPointType, "GAS")
-                        .eq(MonitorPoint::getDelFlag, "0")
-        );
-
-        Set<String> gasParentTypeIds = getGasParentTypeIds();
-
-        List<Map<String, Object>> result = new ArrayList<>();
-        for (MonitorPoint p : points) {
-            Map<String, Object> item = new LinkedHashMap<>();
-            item.put("pointId", p.getPointId());
-            item.put("pointCode", p.getPointCode());
-            item.put("pointName", p.getPointName());
-            item.put("location", p.getLocation());
-            item.put("status", "0");
-            item.put("stationCode", p.getPointCode());
-
-            // 查关联设备
-            List<EquipmentPointRel> rels = equipmentPointRelMapper.selectList(
-                    new LambdaQueryWrapper<EquipmentPointRel>()
-                            .eq(EquipmentPointRel::getPointId, p.getPointId())
-                            .eq(EquipmentPointRel::getDelFlag, "0")
-            );
-
-            if (!rels.isEmpty()) {
-                List<String> equipmentIds = rels.stream().map(EquipmentPointRel::getEquipmentId).collect(Collectors.toList());
-                List<EquipmentBase> equipments = equipmentBaseMapper.selectBatchIds(equipmentIds);
-                item.put("deviceCount", equipments.size());
-
-                for (EquipmentBase eq : equipments) {
-                    if (gasParentTypeIds.contains(eq.getEquipmentTypeId())) {
-                        // 气体设备: mac_address = equipment_code
-                        GasMonitorData latestGas = getLatestGasData(eq.getEquipmentCode());
-                        if (latestGas != null) {
-                            double methane = latestGas.getGasConcentration() != null ? latestGas.getGasConcentration().doubleValue() : 0;
-                            item.put("methaneValue", Math.round(methane * 10.0) / 10.0);
-                            item.put("lastUpdateTime", latestGas.getReportTime() != null ? latestGas.getReportTime().format(DT_FMT) : "");
-                            item.put("alertStatus", methane > 20.0);
-                        }
-                    }
-                    if (eq.getEquipmentTypeId() != null && String.valueOf(eq.getEquipmentTypeId()).contains("ENV")) {
-                        // 环境设备: device_id = Integer(equipment_code)
-                        ERealTimeData latestEnv = getLatestEnvData(parseIntSafe(eq.getEquipmentCode()));
-                        if (latestEnv != null) {
-                            double temp = latestEnv.getTem() != null ? latestEnv.getTem().doubleValue() : 0;
-                            item.put("temperature", Math.round(temp * 10.0) / 10.0);
-                        }
-                    }
-                }
-            }
-
-            // 默认值
-            item.putIfAbsent("methaneValue", 0.0);
-            item.putIfAbsent("methaneThreshold", 20.0);
-            item.putIfAbsent("temperature", 0.0);
-            item.putIfAbsent("lastUpdateTime", "");
-            item.putIfAbsent("deviceCount", 0);
-            item.putIfAbsent("alertStatus", false);
-
-            result.add(item);
-        }
-        return result;
-    }
-
-    @Override
-    public Map<String, Object> getTrend(String pointId, int hours) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        String gasEquipmentCode = findGasEquipmentCode(pointId);
-        if (gasEquipmentCode == null) {
-            result.put("times", new ArrayList<>());
-            result.put("methaneValues", new ArrayList<>());
-            result.put("threshold", 20.0);
-            return result;
-        }
-
-        // 查询该设备的所有数据,按时间排序
-        List<GasMonitorData> allData = gasMonitorDataMapper.selectList(
-                new LambdaQueryWrapper<GasMonitorData>()
-                        .eq(GasMonitorData::getMacAddress, gasEquipmentCode)
-                        .orderByAsc(GasMonitorData::getReportTime)
-        );
-
-        // 找出数据的时间范围
-        LocalDateTime dataStart = allData.isEmpty() ? LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0)
-                : allData.get(0).getReportTime();
-        LocalDateTime dataEnd = allData.isEmpty() ? dataStart.plusHours(23)
-                : allData.get(allData.size() - 1).getReportTime();
-
-        // 按小时分组聚合
-        Map<String, Double> hourMap = new LinkedHashMap<>();
-        for (GasMonitorData d : allData) {
-            if (d.getReportTime() == null || d.getGasConcentration() == null) continue;
-            String hourKey = d.getReportTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH"));
-            hourMap.merge(hourKey, d.getGasConcentration().doubleValue(), (a, b) -> (a + b) / 2);
-        }
-
-        // 构建24小时时间线,从dataStart的0点到dataEnd的23点
-        LocalDateTime rangeStart = dataStart.withHour(0).withMinute(0).withSecond(0).withNano(0);
-        LocalDateTime rangeEnd = dataEnd.withHour(23).withMinute(0).withSecond(0).withNano(0);
-        if (rangeEnd.isBefore(rangeStart.plusHours(23))) {
-            rangeEnd = rangeStart.plusHours(23);
-        }
-
-        List<String> times = new ArrayList<>();
-        List<Double> values = new ArrayList<>();
-        double lastVal = 0;
-        LocalDateTime cursor = rangeStart;
-        while (!cursor.isAfter(rangeEnd)) {
-            times.add(cursor.format(DateTimeFormatter.ofPattern("HH:mm")));
-            String key = cursor.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH"));
-            Double val = hourMap.getOrDefault(key, null);
-            if (val != null) {
-                lastVal = val;
-            }
-            values.add(Math.round(lastVal * 10.0) / 10.0);
-            cursor = cursor.plusHours(1);
-        }
-
-        result.put("times", times);
-        result.put("methaneValues", values);
-        result.put("threshold", 20.0);
-        return result;
-    }
-
-    // ========== 私有辅助方法 ==========
-
-    /** 查监测点关联的气体设备equipment_code */
-    private String findGasEquipmentCode(String pointId) {
-        List<EquipmentPointRel> rels = equipmentPointRelMapper.selectList(
-                new LambdaQueryWrapper<EquipmentPointRel>()
-                        .eq(EquipmentPointRel::getPointId, pointId)
-                        .eq(EquipmentPointRel::getDelFlag, "0")
-        );
-        if (rels.isEmpty()) return null;
-        List<String> eqIds = rels.stream().map(EquipmentPointRel::getEquipmentId).collect(Collectors.toList());
-        List<EquipmentBase> eqs = equipmentBaseMapper.selectBatchIds(eqIds);
-        Set<String> gasParentTypeIds = getGasParentTypeIds();
-        for (EquipmentBase eq : eqs) {
-            if (gasParentTypeIds.contains(eq.getEquipmentTypeId())) {
-                return eq.getEquipmentCode();
-            }
-        }
-        return null;
-    }
-
-    private GasMonitorData getLatestGasData(String macAddress) {
-        List<GasMonitorData> list = gasMonitorDataMapper.selectList(
-                new LambdaQueryWrapper<GasMonitorData>()
-                        .eq(GasMonitorData::getMacAddress, macAddress)
-                        .orderByDesc(GasMonitorData::getReportTime)
-                        .last("limit 1")
-        );
-        return list.isEmpty() ? null : list.get(0);
-    }
-
-    private ERealTimeData getLatestEnvData(int deviceId) {
-        List<ERealTimeData> list = eRealTimeDataMapper.selectList(
-                new LambdaQueryWrapper<ERealTimeData>()
-                        .eq(ERealTimeData::getDeviceId, deviceId)
-                        .orderByDesc(ERealTimeData::getCreateTime)
-                        .last("limit 1")
-        );
-        return list.isEmpty() ? null : list.get(0);
-    }
-
-    private int parseIntSafe(String s) {
-        try { return Integer.parseInt(s); } catch (Exception e) { return 0; }
-    }
-
-    /**
-     * 查询父类ID为3(燃气)的所有设备类型ID集合
-     */
-    private Set<String> getGasParentTypeIds() {
-        List<EquipmentType> gasTypes = equipmentTypeMapper.selectList(
-                new LambdaQueryWrapper<EquipmentType>()
-                        .eq(EquipmentType::getParentId, GAS_parent_id));
-        return gasTypes.stream()
-                .map(type -> String.valueOf(type.getId()))
-                .collect(Collectors.toSet());
-    }
-}

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/service/WorkOrderService.java

@@ -80,7 +80,7 @@ public interface WorkOrderService extends IService<WorkOrder> {
      * 从报警记录创建工单(初始状态:待派单)。
      *
      * <p>同一报警记录重复调用时返回已有工单 ID,不重复建单;报警记录与工单的
-     * 状态回写由本方法在同一事务内完成。</p>
+     * 关联在同一事务内完成;创建工单不代表报警已处理,不修改设备状态。</p>
      *
      * @param alarmId    报警记录 ID
      * @param orderLevel 工单优先级,传空时按报警等级映射

+ 21 - 28
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/service/impl/WorkOrderServiceImpl.java

@@ -309,15 +309,6 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         // 报警转工单必须幂等:重复点击直接返回已有工单,不重复生成工单。
         WorkOrder existing = alarm.getOrderId() == null ? null : getById(alarm.getOrderId());
         if (existing != null) {
-            if (!Objects.equals(alarm.getAlarmStatus(), 1)) {
-                LocalDateTime now = LocalDateTime.now();
-                alarm.setAlarmStatus(1);
-                alarm.setHandleTime(now);
-                alarm.setHandleUser(currentUserName());
-                alarm.setHandleRemark("已关联工单:" + existing.getOrderNo());
-                alarm.setUpdateTime(now);
-                alarmDataService.updateById(alarm);
-            }
             return existing.getOrderId();
         }
         if (alarm.getOrderId() != null) {
@@ -354,21 +345,20 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         workOrder.setUpdateTime(now);
         this.save(workOrder);
 
-        alarm.setOrderId(workOrder.getOrderId());
-
-        writeLog(workOrder.getOrderId(), WorkOrderLogOperTypeEnum.CREATE,
-                buildDesc("由报警[" + alarmId + "]创建工单", workOrder.getOrderDesc()), null);
-
-        LocalDateTime alarmUpdateTime = LocalDateTime.now();
-        alarm.setAlarmStatus(1);
-        alarm.setHandleTime(alarmUpdateTime);
-        alarm.setHandleUser(currentUserName());
-        alarm.setHandleRemark("已派发工单:" + workOrder.getOrderNo());
-        alarm.setUpdateTime(alarmUpdateTime);
-        if (!alarmDataService.update(alarm, new LambdaQueryWrapper<AlarmData>()
+        // 关联工单不等于异常已经恢复,仅更新关联;不覆盖审核结论、报警处理状态或设备状态。
+        AlarmData relation = new AlarmData();
+        relation.setOrderId(workOrder.getOrderId());
+        relation.setUpdateTime(LocalDateTime.now());
+        if (!alarmDataService.update(relation, new LambdaQueryWrapper<AlarmData>()
                 .eq(AlarmData::getId, alarmId).isNull(AlarmData::getOrderId))) {
             throw new ServiceException("报警已关联工单或已被删除,请刷新后重试");
         }
+        // 保留报警生成时已有的井盖案件关联,案件按设备主键关联,不复制旧设备编码。
+        caseInfoMapper.update(null, new LambdaUpdateWrapper<CaseInfo>()
+                .eq(CaseInfo::getAlarmId, alarmId).isNull(CaseInfo::getOrderId)
+                .set(CaseInfo::getOrderId, workOrder.getOrderId()));
+        writeLog(workOrder.getOrderId(), WorkOrderLogOperTypeEnum.CREATE,
+                buildDesc("由报警[" + alarmId + "]创建工单", workOrder.getOrderDesc()), null);
         return workOrder.getOrderId();
     }
 
@@ -884,13 +874,16 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     private String buildAlarmOrderDesc(AlarmData alarm) {
         StringBuilder sb = new StringBuilder();
         sb.append(StringUtils.isEmpty(alarm.getWarningType()) ? "监测报警" : alarm.getWarningType());
-        sb.append(",当前值:").append(alarm.getActualValue() == null ? "-" : alarm.getActualValue().toPlainString());
-        if (alarm.getMinValue() != null || alarm.getMaxValue() != null) {
-            sb.append("(阈值:")
-                    .append(alarm.getMinValue() == null ? "-" : alarm.getMinValue().toPlainString())
-                    .append("~")
-                    .append(alarm.getMaxValue() == null ? "-" : alarm.getMaxValue().toPlainString())
-                    .append(")");
+        // 状态类报警可能没有实际值,此时不拼接无意义的数值和阈值占位。
+        if (alarm.getActualValue() != null) {
+            sb.append(",当前值:").append(alarm.getActualValue().toPlainString());
+            if (alarm.getMinValue() != null || alarm.getMaxValue() != null) {
+                sb.append("(阈值:")
+                        .append(alarm.getMinValue() == null ? "-" : alarm.getMinValue().toPlainString())
+                        .append("~")
+                        .append(alarm.getMaxValue() == null ? "-" : alarm.getMaxValue().toPlainString())
+                        .append(")");
+            }
         }
         sb.append(",请现场核实处理");
         return sb.toString();

+ 57 - 0
pipe-network-service/zksy-system/src/main/resources/mapper/base/AlarmReviewMapper.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.zksy.base.alarm.mapper.AlarmReviewMapper">
+    <!-- EXISTS 保证一条报警一行;不按设备去重,否则会丢失待审核事件。 -->
+    <sql id="scope">
+        <choose>
+            <when test="typeIds != null and typeIds.size() > 0">
+                AND EXISTS (SELECT 1 FROM app_user.equipment_base e
+                    WHERE e.equipment_code = a.device_code AND e.equipment_type_id IN
+                    <foreach collection="typeIds" item="typeId" open="(" separator="," close=")">#{typeId}</foreach>)
+            </when>
+            <otherwise>AND 1 = 0</otherwise>
+        </choose>
+        <if test="query.warningCode != null and query.warningCode != ''">AND a.warning_code = #{query.warningCode}</if>
+    </sql>
+    <sql id="filters">
+        AND a.alarm_status = #{unhandled}
+        <include refid="scope"/>
+        <if test="query.keyword != null and query.keyword != ''">
+            AND (a.device_code ILIKE CONCAT('%', #{query.keyword}, '%')
+                OR EXISTS (SELECT 1 FROM app_user.equipment_base e WHERE e.equipment_code = a.device_code
+                    AND e.equipment_name ILIKE CONCAT('%', #{query.keyword}, '%')))
+        </if>
+        <if test="query.alarmLevel != null">AND a.alarm_level = #{query.alarmLevel}</if>
+        <if test="query.startTime != null">AND a.alarm_time &gt;= #{query.startTime}</if>
+        <if test="query.endTime != null">AND a.alarm_time &lt;= #{query.endTime}</if>
+    </sql>
+    <sql id="columns">
+        a.*,
+        (SELECT e.equipment_id FROM app_user.equipment_base e WHERE e.equipment_code = a.device_code ORDER BY e.equipment_id LIMIT 1) AS equipment_id,
+        (SELECT e.equipment_location FROM app_user.equipment_base e WHERE e.equipment_code = a.device_code ORDER BY e.equipment_id LIMIT 1) AS equipment_location,
+        (SELECT e.equipment_name FROM app_user.equipment_base e WHERE e.equipment_code = a.device_code ORDER BY e.equipment_id LIMIT 1) AS current_device_name,
+        (SELECT w.warning_unit FROM app_user.equipment_warning_type w
+            JOIN app_user.equipment_base e ON e.equipment_type_id = w.equipment_type_id
+            WHERE e.equipment_code = a.device_code AND w.warning_code = a.warning_code
+            ORDER BY e.equipment_id LIMIT 1) AS warning_unit
+    </sql>
+    <resultMap id="review" type="com.zksy.base.alarm.dto.AlarmReviewVO" autoMapping="true">
+        <id property="id" column="id"/>
+        <result property="deviceName" column="current_device_name"/>
+    </resultMap>
+    <select id="selectPage" resultMap="review">
+        SELECT <include refid="columns"/> FROM app_user.alarm_data a
+        <where>
+            <include refid="filters"/>
+            <if test="query.auditStatus != null">AND a.audit_status = #{query.auditStatus}</if>
+        </where>
+        ORDER BY a.alarm_time <choose><when test="query.ascending">ASC</when><otherwise>DESC</otherwise></choose> NULLS LAST, a.id DESC
+    </select>
+    <select id="selectDetail" resultMap="review">
+        SELECT <include refid="columns"/> FROM app_user.alarm_data a WHERE a.id = #{id} <include refid="scope"/>
+    </select>
+    <select id="selectStatistics" resultType="java.util.Map">
+        SELECT COUNT(*) AS total, COUNT(CASE WHEN a.audit_status = #{unreviewed} THEN 1 END) AS pending
+        FROM app_user.alarm_data a <where><include refid="filters"/></where>
+    </select>
+</mapper>

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

@@ -1,391 +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.conditions.Wrapper;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmAuditInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmDispatchInDTO;
-import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOrderStatVO;
-import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
-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.alarm.service.AlarmClearService;
-import com.zksy.base.domain.EquipmentBase;
-import com.zksy.base.domain.EquipmentStatus;
-import com.zksy.base.domain.EquipmentType;
-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.common.exception.ServiceException;
-import com.zksy.common.core.domain.model.LoginUser;
-import com.zksy.common.core.domain.entity.SysUser;
-import com.zksy.ops.domain.WorkOrder;
-import com.zksy.ops.domain.WorkOrderLog;
-import com.zksy.ops.enums.WorkOrderStatusEnum;
-import com.zksy.ops.mapper.WorkOrderLogMapper;
-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 org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.context.SecurityContextHolder;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * 报警审核转工单闭环契约测试(T03 / Issue #46)。
- *
- * <p>被测公共 seam:{@code IWaterSupplyAlarmService#auditAlarm(AlarmAuditInDTO)} 与
- * {@code #dispatchToWorkOrder(AlarmDispatchInDTO)}。断言只针对外部可观察结果:
- * 是否抛错、是否创建工单、工单的类型/状态/接单人。</p>
- */
-class WaterSupplyAlarmAuditServiceTest {
-
-    private static final String DEVICE_CODE = "CN-GS-202609160001";
-    private static final String WARNING_TYPE = "压力预警";
-
-    @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);
-        TableInfoHelper.initTableInfo(assistant, WorkOrderLog.class);
-        TableInfoHelper.initTableInfo(assistant, AlarmAudit.class);
-        TableInfoHelper.initTableInfo(assistant, EquipmentType.class);
-    }
-
-    private WaterSupplyAlarmServiceImpl service;
-    private AlarmDataService alarmDataService;
-    private AlarmDataMapper alarmDataMapper;
-    private EquipmentBaseService equipmentBaseService;
-    private EquipmentBaseMapper equipmentBaseMapper;
-    private WorkOrderMapper workOrderMapper;
-    private WorkOrderLogMapper workOrderLogMapper;
-    private AlarmAuditMapper alarmAuditMapper;
-    private EquipmentStatusMapper equipmentStatusMapper;
-    private EquipmentTypeMapper equipmentTypeMapper;
-    private AlarmClearService alarmClearService;
-
-    @BeforeEach
-    void setUp() {
-        // 为 SecurityUtils 提供登录态
-        SysUser user = new SysUser();
-        user.setUserId(7L);
-        user.setUserName("operator");
-        user.setNickName("调度管理员");
-        LoginUser loginUser = new LoginUser(7L, 1L, user, Collections.emptySet());
-        SecurityContextHolder.getContext().setAuthentication(
-                new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()));
-
-        service = new WaterSupplyAlarmServiceImpl();
-        alarmDataService = mock(AlarmDataService.class);
-        alarmDataMapper = mock(AlarmDataMapper.class);
-        equipmentBaseService = mock(EquipmentBaseService.class);
-        equipmentBaseMapper = mock(EquipmentBaseMapper.class);
-        workOrderMapper = mock(WorkOrderMapper.class);
-        workOrderLogMapper = mock(WorkOrderLogMapper.class);
-        alarmAuditMapper = mock(AlarmAuditMapper.class);
-        equipmentStatusMapper = mock(EquipmentStatusMapper.class);
-        equipmentTypeMapper = mock(EquipmentTypeMapper.class);
-        alarmClearService = mock(AlarmClearService.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);
-        ReflectionTestUtils.setField(service, "workOrderLogMapper", workOrderLogMapper);
-        ReflectionTestUtils.setField(service, "alarmAuditMapper", alarmAuditMapper);
-        ReflectionTestUtils.setField(service, "equipmentTypeMapper", equipmentTypeMapper);
-        ReflectionTestUtils.setField(service, "alarmClearService", alarmClearService);
-
-        when(equipmentBaseMapper.selectOne(any())).thenReturn(device());
-        when(equipmentBaseMapper.selectList(any())).thenReturn(Collections.singletonList(device()));
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(equipmentTypeMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(workOrderLogMapper.insert(any(WorkOrderLog.class))).thenReturn(1);
-        when(workOrderMapper.insert(any(WorkOrder.class))).thenAnswer(invocation -> {
-            WorkOrder order = invocation.getArgument(0);
-            order.setOrderId(900L);
-            return 1;
-        });
-        when(alarmDataService.updateById(any())).thenReturn(true);
-        when(alarmDataService.update(any(AlarmData.class), any())).thenReturn(true);
-    }
-
-    private static EquipmentBase device() {
-        EquipmentBase device = new EquipmentBase();
-        device.setEquipmentId("EQ-PR-1");
-        device.setEquipmentCode(DEVICE_CODE);
-        device.setEquipmentName("城南加压站压力监测点");
-        device.setEquipmentTypeId(12);
-        return device;
-    }
-
-    private static AlarmData alarm(String id, int level) {
-        AlarmData alarm = new AlarmData();
-        alarm.setId(id);
-        alarm.setDeviceCode(DEVICE_CODE);
-        alarm.setDeviceName("城南加压站压力监测点");
-        alarm.setDeviceType("压力监测");
-        alarm.setWarningType(WARNING_TYPE);
-        alarm.setWarningCode("T45-WARN-PRESSURE");
-        alarm.setActualValue(new BigDecimal("0.95"));
-        alarm.setMinValue(new BigDecimal("0.40"));
-        alarm.setMaxValue(new BigDecimal("0.80"));
-        alarm.setAlarmStatus(0);
-        alarm.setAlarmLevel(level);
-        alarm.setAlarmTime(LocalDateTime.now());
-        return alarm;
-    }
-
-    @Test
-    void transferCreatesFaultRepairDraftOrderWithoutReceiver() {
-        AlarmData target = alarm("a1", 1);
-        when(alarmDataService.getById("a1")).thenReturn(target);
-        // 该设备该报警类型没有未结束工单
-        when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(new ArrayList<>(
-                Collections.singletonList(target)));
-        when(workOrderMapper.selectCount(any())).thenReturn(0L);
-
-        AlarmAuditInDTO in = new AlarmAuditInDTO();
-        in.setAlarmId("a1");
-        in.setAuditAction(1);
-        in.setAuditResult(1);
-        in.setAuditOpinion("现场核实属实,转故障维修");
-
-        service.auditAlarm(in);
-
-        ArgumentCaptor<WorkOrder> captor = ArgumentCaptor.forClass(WorkOrder.class);
-        verify(workOrderMapper).insert(captor.capture());
-        WorkOrder created = captor.getValue();
-        // 工单只保留设备ID,报警关联通过 alarm_data.order_id 回查。
-        assertEquals("EQ-PR-1", created.getDeviceId());
-        assertEquals(0, created.getOrderStatus());
-        assertNull(created.getDispatchTime());
-        // 部门隔离兜底:报警转工单必须归属当前用户部门,否则统一工单页按部门过滤后查不到,闭环断裂
-        assertEquals(1L, created.getDeptId());
-        assertNull(created.getReceiveUser());
-        assertEquals(created.getOrderId(), target.getOrderId());
-        verify(alarmDataService).update(org.mockito.ArgumentMatchers.eq(target), any());
-    }
-
-    @Test
-    void transferIsRejectedWhenSameDeviceAndWarningTypeHasUnfinishedOrder() {
-        AlarmData target = alarm("a2", 2);
-        when(alarmDataService.getById("a2")).thenReturn(target);
-        // 同设备同报警类型的另一条报警 + 一张未结束工单(处理中)
-        AlarmData existing = alarm("a1", 1);
-        existing.setOrderId(901L);
-        when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(Arrays.asList(
-                target, existing));
-        when(workOrderMapper.selectCount(any())).thenReturn(1L);
-
-        AlarmAuditInDTO in = new AlarmAuditInDTO();
-        in.setAlarmId("a2");
-        in.setAuditAction(1);
-        in.setAuditResult(1);
-
-        ServiceException error = assertThrows(ServiceException.class, () -> service.auditAlarm(in));
-        assertEquals("该设备同一报警类型已存在未结束的故障维修工单,不能重复创建", error.getMessage());
-        verify(workOrderMapper, never()).insert(any(WorkOrder.class));
-    }
-
-    @Test
-    void unfinishedCheckExcludesClosedAndCancelledOrders() {
-        AlarmData target = alarm("a3", 3);
-        when(alarmDataService.getById("a3")).thenReturn(target);
-        AlarmData closedSource = alarm("closed", 3);
-        closedSource.setOrderId(902L);
-        when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(
-                new ArrayList<>(Arrays.asList(target, closedSource)));
-        when(workOrderMapper.selectCount(any())).thenReturn(0L);
-
-        service.dispatchToWorkOrder(dispatch("a3"));
-
-        ArgumentCaptor<Wrapper<WorkOrder>> wrapperCaptor = wrapperCaptor();
-        verify(workOrderMapper).selectCount(wrapperCaptor.capture());
-        AbstractWrapper<?, ?, ?> wrapper = (AbstractWrapper<?, ?, ?>) wrapperCaptor.getValue();
-        // 参数映射惰性物化,先取一次 SQL 片段再断言绑定值
-        wrapper.getSqlSegment();
-        // 唯一性口径由权威枚举的未办结状态集合决定:含全部 activeCodes,且不含任何终态码
-        for (Integer active : WorkOrderStatusEnum.activeCodes()) {
-            assertTrueContainsValue(wrapper, active);
-        }
-        for (Integer finished : WorkOrderStatusEnum.finishedCodes()) {
-            if (wrapper.getParamNameValuePairs().containsValue(finished)) {
-                throw new AssertionError("未办结判定不应包含终态码:" + finished);
-            }
-        }
-    }
-
-    @Test
-    void falseAlarmReleaseRecordsOpinionAndCreatesNoOrder() {
-        AlarmData target = alarm("a4", 2);
-        when(alarmDataService.getById("a4")).thenReturn(target);
-
-        AlarmAuditInDTO in = new AlarmAuditInDTO();
-        in.setAlarmId("a4");
-        in.setAuditAction(2);
-        in.setAuditResult(2);
-        in.setAuditOpinion("现场核实为误报");
-        in.setAttachUrl("https://example.com/evidence.jpg");
-
-        service.auditAlarm(in);
-
-        ArgumentCaptor<AlarmAudit> auditCaptor = ArgumentCaptor.forClass(AlarmAudit.class);
-        verify(alarmAuditMapper).insert(auditCaptor.capture());
-        assertEquals("现场核实为误报", auditCaptor.getValue().getAuditOpinion());
-        assertEquals("https://example.com/evidence.jpg", auditCaptor.getValue().getAttachUrl());
-        verify(workOrderMapper, never()).insert(any(WorkOrder.class));
-        // 误报解除只触发清警复核,不直接改设备报警状态(ADR-0002 / #51)
-        verify(alarmClearService).review(DEVICE_CODE, "误报解除");
-        verify(equipmentStatusMapper, never()).updateById(any(EquipmentStatus.class));
-    }
-
-    @Test
-    void auditRejectsActionOutsideTransferAndFalseAlarmRelease() {
-        AlarmAuditInDTO in = new AlarmAuditInDTO();
-        in.setAlarmId("a5");
-        in.setAuditAction(9);
-        in.setAuditResult(1);
-
-        ServiceException error = assertThrows(ServiceException.class, () -> service.auditAlarm(in));
-        assertEquals("审核动作仅支持 1-转工单 或 2-误报解除", error.getMessage());
-        verify(alarmAuditMapper, never()).insert(any(AlarmAudit.class));
-    }
-
-    /**
-     * 报警审核页顶部状态计数(#46 验收项):全部 / 待处理(0,1) / 处理中(2,3,6) /
-     * 待验收(4) / 已完成(5,7),分组口径取自权威工单状态枚举。
-     */
-    @Test
-    void auditOrderStatsGroupWaterAlarmOrdersByWorkOrderStatus() {
-        when(equipmentBaseService.findByTopLevelType("供水"))
-                .thenReturn(Collections.singletonList(device()));
-        List<Map<String, Object>> rows = new ArrayList<>();
-        rows.add(statusRow(0, 2L));
-        rows.add(statusRow(1, 1L));
-        rows.add(statusRow(3, 4L));
-        rows.add(statusRow(4, 3L));
-        rows.add(statusRow(5, 5L));
-        rows.add(statusRow(7, 1L));
-        // 未知状态码(99)必须同时不进分组、也不进总数
-        rows.add(statusRow(99, 7L));
-        rows.add(statusRow(null, 3L));
-        when(workOrderMapper.countByStatus(any())).thenReturn(rows);
-
-        AlarmAuditOrderStatVO stats = service.getAuditOrderStats();
-
-        assertEquals(16L, stats.getTotal());
-        assertEquals(3L, stats.getPendingCount());
-        assertEquals(4L, stats.getProcessingCount());
-        assertEquals(3L, stats.getVerifyingCount());
-        assertEquals(6L, stats.getFinishedCount());
-    }
-
-    @Test
-    void auditOrderStatsOnlyCountsAlarmSourcedOrdersOfWaterDevices() {
-        when(equipmentBaseService.findByTopLevelType("供水"))
-                .thenReturn(Collections.singletonList(device()));
-        when(workOrderMapper.countByStatus(any())).thenReturn(Collections.emptyList());
-
-        service.getAuditOrderStats();
-
-        ArgumentCaptor<Wrapper<WorkOrder>> captor = wrapperCaptor();
-        verify(workOrderMapper).countByStatus(captor.capture());
-        AbstractWrapper<?, ?, ?> wrapper = (AbstractWrapper<?, ?, ?>) captor.getValue();
-        // 仅统计报警关联的工单,不能把手工工单混入审核统计。
-        assertTrue(wrapper.getSqlSegment().contains("alarm_data"));
-        assertTrue(wrapper.getSqlSegment().contains("a.order_id = app_user.work_order.order_id"));
-        assertTrue(wrapper.getSqlSegment().contains("device_id"));
-        assertTrueContainsValue(wrapper, "EQ-PR-1");
-    }
-
-    @Test
-    void auditOrderStatsReturnsZerosWithoutWaterDevices() {
-        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Collections.emptyList());
-
-        AlarmAuditOrderStatVO stats = service.getAuditOrderStats();
-
-        assertEquals(0L, stats.getTotal());
-        assertEquals(0L, stats.getPendingCount());
-        verify(workOrderMapper, never()).selectMaps(any());
-    }
-
-    private static Map<String, Object> statusRow(Integer status, long count) {
-        Map<String, Object> row = new LinkedHashMap<>();
-        row.put("status", status);
-        row.put("cnt", count);
-        return row;
-    }
-
-    @Test
-    void repeatedTransferUsesOrderIdWithoutOverwritingExistingRelationship() {
-        AlarmData target = alarm("already-linked", 1);
-        target.setOrderId(901L);
-        WorkOrder existing = new WorkOrder();
-        existing.setOrderId(901L);
-        when(alarmDataService.getById(target.getId())).thenReturn(target);
-        when(workOrderMapper.selectById(901L)).thenReturn(existing);
-        assertTrue(service.dispatchToWorkOrder(dispatch(target.getId())));
-        verify(workOrderMapper, never()).insert(any(WorkOrder.class));
-        assertEquals(901L, target.getOrderId());
-    }
-
-    @Test
-    void failedConditionalBindingRejectsConcurrentCreation() {
-        AlarmData target = alarm("race", 1);
-        when(alarmDataService.getById("race")).thenReturn(target);
-        when(alarmDataService.update(any(AlarmData.class), any())).thenReturn(false);
-        assertThrows(ServiceException.class, () -> service.dispatchToWorkOrder(dispatch("race")));
-    }
-
-    private AlarmDispatchInDTO dispatch(String alarmId) {
-        AlarmDispatchInDTO dispatch = new AlarmDispatchInDTO();
-        dispatch.setAlarmId(alarmId);
-        return dispatch;
-    }
-
-    @SuppressWarnings("unchecked")
-    private static ArgumentCaptor<Wrapper<WorkOrder>> wrapperCaptor() {
-        return (ArgumentCaptor<Wrapper<WorkOrder>>) (ArgumentCaptor<?>) ArgumentCaptor.forClass(Wrapper.class);
-    }
-
-    private static void assertTrueContainsValue(AbstractWrapper<?, ?, ?> wrapper, Object expected) {
-        if (!wrapper.getParamNameValuePairs().containsValue(expected)) {
-            throw new AssertionError("查询条件缺少排除值:" + expected
-                    + ",实际:" + wrapper.getParamNameValuePairs());
-        }
-    }
-}

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

@@ -3,9 +3,7 @@ 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.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmDashboardOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
 import com.zksy.WaterSupply.MonitorAlarm.service.impl.WaterSupplyAlarmServiceImpl;
 import com.zksy.base.service.impl.WaterSupplyQueryService;
 import com.zksy.base.alarm.domain.AlarmData;
@@ -55,7 +53,6 @@ class WaterSupplyAlarmDashboardServiceTest {
         TableInfoHelper.initTableInfo(assistant, EquipmentBase.class);
         TableInfoHelper.initTableInfo(assistant, EquipmentStatus.class);
         TableInfoHelper.initTableInfo(assistant, WorkOrder.class);
-        TableInfoHelper.initTableInfo(assistant, AlarmAudit.class);
         TableInfoHelper.initTableInfo(assistant, EquipmentType.class);
     }
 
@@ -67,7 +64,6 @@ class WaterSupplyAlarmDashboardServiceTest {
     private EquipmentStatusMapper equipmentStatusMapper;
     private EquipmentTypeMapper equipmentTypeMapper;
     private WorkOrderMapper workOrderMapper;
-    private AlarmAuditMapper alarmAuditMapper;
 
     @BeforeEach
     void setUp() {
@@ -79,7 +75,6 @@ class WaterSupplyAlarmDashboardServiceTest {
         equipmentStatusMapper = mock(EquipmentStatusMapper.class);
         equipmentTypeMapper = mock(EquipmentTypeMapper.class);
         workOrderMapper = mock(WorkOrderMapper.class);
-        alarmAuditMapper = mock(AlarmAuditMapper.class);
         ReflectionTestUtils.setField(service, "alarmDataService", alarmDataService);
         ReflectionTestUtils.setField(service, "waterSupplyQueryService",
                 new WaterSupplyQueryService(equipmentBaseService, equipmentBaseMapper,
@@ -88,7 +83,6 @@ class WaterSupplyAlarmDashboardServiceTest {
         ReflectionTestUtils.setField(service, "equipmentBaseMapper", equipmentBaseMapper);
         ReflectionTestUtils.setField(service, "equipmentTypeMapper", equipmentTypeMapper);
         ReflectionTestUtils.setField(service, "workOrderMapper", workOrderMapper);
-        ReflectionTestUtils.setField(service, "alarmAuditMapper", alarmAuditMapper);
 
         EquipmentBase pressure = device("EQ-P-1", "CN-GS-202609020001", "1");
         EquipmentBase flow = device("EQ-F-1", "CN-GS-202609020002", "2");
@@ -96,7 +90,6 @@ class WaterSupplyAlarmDashboardServiceTest {
         when(equipmentBaseMapper.selectList(any())).thenReturn(Arrays.asList(pressure, flow));
         when(equipmentTypeMapper.selectBatchIds(any())).thenReturn(Arrays.asList(
                 type(1, "压力监测"), type(2, "流量监测")));
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
     }
 
     /** 设备类型ID 现为整型;非数字按 null 处理。 */
@@ -149,7 +142,7 @@ class WaterSupplyAlarmDashboardServiceTest {
                 alarm("a1", "CN-GS-202609020001", "压力预警", 0, 1, today),
                 alarm("a2", "CN-GS-202609020001", "压力预警", 1, 2, today.plusHours(1)),
                 // 昨日:流量预警 3 级,误报解除(已解除)
-                alarm("a3", "CN-GS-202609020002", "流量预警", 0, 3, today.minusDays(1))));
+                alarm("a3", "CN-GS-202609020002", "流量预警", 1, 3, today.minusDays(1))));
         when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(alarms);
 
         alarms.get(1).setOrderId(202L);
@@ -160,11 +153,7 @@ class WaterSupplyAlarmDashboardServiceTest {
         order.setOrderStatus(3);
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(order));
 
-        AlarmAudit released = new AlarmAudit();
-        released.setAlarmId("a3");
-        released.setAuditAction(2);
-        released.setAuditResult(2);
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.singletonList(released));
+        alarms.get(2).setAuditStatus(com.zksy.base.alarm.enums.AlarmAuditStatusEnum.FALSE_ALARM.getCode());
 
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-P-1", 1, 1, 1)));

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

@@ -4,9 +4,7 @@ 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.domain.AlarmAudit;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
 import com.zksy.WaterSupply.MonitorAlarm.service.impl.WaterSupplyAlarmServiceImpl;
 import com.zksy.base.service.impl.WaterSupplyQueryService;
 import com.zksy.base.alarm.domain.AlarmData;
@@ -55,7 +53,6 @@ class WaterSupplyAlarmLedgerServiceTest {
         TableInfoHelper.initTableInfo(assistant, EquipmentBase.class);
         TableInfoHelper.initTableInfo(assistant, EquipmentStatus.class);
         TableInfoHelper.initTableInfo(assistant, WorkOrder.class);
-        TableInfoHelper.initTableInfo(assistant, AlarmAudit.class);
     }
 
     private WaterSupplyAlarmServiceImpl service;
@@ -66,7 +63,6 @@ class WaterSupplyAlarmLedgerServiceTest {
     private EquipmentStatusMapper equipmentStatusMapper;
     private EquipmentTypeMapper equipmentTypeMapper;
     private WorkOrderMapper workOrderMapper;
-    private AlarmAuditMapper alarmAuditMapper;
 
     private EquipmentBase pressureDevice;
     private EquipmentBase flowDevice;
@@ -81,7 +77,6 @@ class WaterSupplyAlarmLedgerServiceTest {
         equipmentStatusMapper = mock(EquipmentStatusMapper.class);
         equipmentTypeMapper = mock(EquipmentTypeMapper.class);
         workOrderMapper = mock(WorkOrderMapper.class);
-        alarmAuditMapper = mock(AlarmAuditMapper.class);
         ReflectionTestUtils.setField(service, "alarmDataService", alarmDataService);
         ReflectionTestUtils.setField(service, "waterSupplyQueryService",
                 new WaterSupplyQueryService(equipmentBaseService, equipmentBaseMapper,
@@ -89,7 +84,6 @@ class WaterSupplyAlarmLedgerServiceTest {
                         alarmDataMapper));
         ReflectionTestUtils.setField(service, "equipmentBaseMapper", equipmentBaseMapper);
         ReflectionTestUtils.setField(service, "workOrderMapper", workOrderMapper);
-        ReflectionTestUtils.setField(service, "alarmAuditMapper", alarmAuditMapper);
 
         pressureDevice = device("EQ-P-1", "CN-GS-202609020001", "城南压力监测点");
         flowDevice = device("EQ-F-1", "CN-GS-202609020002", "城北流量监测点");
@@ -157,7 +151,6 @@ class WaterSupplyAlarmLedgerServiceTest {
     void pendingAlarmWithoutAuditOrOrderShowsPendingAudit() {
         stubLedger(alarm("alarm-1", "CN-GS-202609020001", "压力预警", 0, 1));
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-P-1", 1, 1, 0)));
 
@@ -183,7 +176,6 @@ class WaterSupplyAlarmLedgerServiceTest {
         order.setOrderNo("WO202606101703482640");
         order.setOrderStatus(3);
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(order));
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-P-1", 1, 1, 0)));
 
@@ -195,21 +187,18 @@ class WaterSupplyAlarmLedgerServiceTest {
     }
 
     @Test
-    void falseAlarmAuditWithoutOrderShowsReleased() {
-        stubLedger(alarm("alarm-1", "CN-GS-202609020002", "流量预警", 0, 2));
+    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());
-        AlarmAudit audit = new AlarmAudit();
-        audit.setAlarmId("alarm-1");
-        audit.setAuditAction(2);
-        audit.setAuditResult(2);
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.singletonList(audit));
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-F-1", 0, 1, 1)));
 
         WaterAlarmOutDTO row = ledgerPage().getRecords().get(0);
 
-        assertEquals(3, row.getDisplayStatus());
-        assertEquals("已解除", row.getDisplayStatusText());
+        assertEquals(4, row.getDisplayStatus());
+        assertEquals("误报警", row.getDisplayStatusText());
         // 报警记录自身仍未处理,但设备已不在报警中:展示状态与设备报警状态各自独立
         assertEquals(0, row.getDeviceAlarmStatus());
         assertEquals("正常", row.getDeviceAlarmStatusText());
@@ -219,7 +208,6 @@ class WaterSupplyAlarmLedgerServiceTest {
     void ledgerQueryIsScopedToWaterDevices() {
         stubLedger();
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.emptyList());
 
         ledgerPage();

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

@@ -3,9 +3,7 @@ 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.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
 import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
-import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
 import com.zksy.WaterSupply.MonitorAlarm.service.impl.WaterSupplyAlarmServiceImpl;
 import com.zksy.base.service.impl.WaterSupplyQueryService;
 import com.zksy.base.alarm.domain.AlarmData;
@@ -54,7 +52,6 @@ class WaterSupplyAlarmRealtimeContractTest {
         TableInfoHelper.initTableInfo(assistant, EquipmentBase.class);
         TableInfoHelper.initTableInfo(assistant, EquipmentStatus.class);
         TableInfoHelper.initTableInfo(assistant, WorkOrder.class);
-        TableInfoHelper.initTableInfo(assistant, AlarmAudit.class);
     }
 
     private WaterSupplyAlarmServiceImpl service;
@@ -65,7 +62,6 @@ class WaterSupplyAlarmRealtimeContractTest {
     private EquipmentStatusMapper equipmentStatusMapper;
     private EquipmentTypeMapper equipmentTypeMapper;
     private WorkOrderMapper workOrderMapper;
-    private AlarmAuditMapper alarmAuditMapper;
 
     @BeforeEach
     void setUp() {
@@ -77,7 +73,6 @@ class WaterSupplyAlarmRealtimeContractTest {
         equipmentStatusMapper = mock(EquipmentStatusMapper.class);
         equipmentTypeMapper = mock(EquipmentTypeMapper.class);
         workOrderMapper = mock(WorkOrderMapper.class);
-        alarmAuditMapper = mock(AlarmAuditMapper.class);
         ReflectionTestUtils.setField(service, "alarmDataService", alarmDataService);
         ReflectionTestUtils.setField(service, "waterSupplyQueryService",
                 new WaterSupplyQueryService(equipmentBaseService, equipmentBaseMapper,
@@ -85,7 +80,6 @@ class WaterSupplyAlarmRealtimeContractTest {
                         alarmDataMapper));
         ReflectionTestUtils.setField(service, "equipmentBaseMapper", equipmentBaseMapper);
         ReflectionTestUtils.setField(service, "workOrderMapper", workOrderMapper);
-        ReflectionTestUtils.setField(service, "alarmAuditMapper", alarmAuditMapper);
 
         EquipmentBase device = device("EQ-P-1", "CN-GS-202609020001", "城南压力监测点");
         when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Collections.singletonList(device));
@@ -137,7 +131,6 @@ class WaterSupplyAlarmRealtimeContractTest {
         order.setOrderNo("WO202609151700001234");
         order.setOrderStatus(3);
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(order));
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-P-1", 1, 1, 1)));
 
@@ -157,7 +150,6 @@ class WaterSupplyAlarmRealtimeContractTest {
         when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(
                 Collections.singletonList(alarm("alarm-2", "CN-GS-202609020001", "流量异常", 0, 3)));
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-P-1", 1, 1, 1)));
 
@@ -187,7 +179,6 @@ class WaterSupplyAlarmRealtimeContractTest {
         order.setOrderNo("WO202609151800002345");
         order.setOrderStatus(3);
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(order));
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-P-1", 1, 1, 1)));
 
@@ -206,7 +197,6 @@ class WaterSupplyAlarmRealtimeContractTest {
         when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(
                 Collections.singletonList(alarm("alarm-4", "CN-GS-202609020001", "流量异常", 1, 3)));
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
                 status("EQ-P-1", 0, 1, 1)));
 

+ 1 - 17
pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmClearServiceTest.java

@@ -3,8 +3,6 @@ package com.zksy.base.alarm.service;
 import com.baomidou.mybatisplus.core.MybatisConfiguration;
 import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
-import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.service.impl.AlarmClearServiceImpl;
 import com.zksy.base.domain.EquipmentBase;
@@ -43,7 +41,6 @@ class AlarmClearServiceTest {
     static void initLambdaCache() {
         MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
         TableInfoHelper.initTableInfo(assistant, AlarmData.class);
-        TableInfoHelper.initTableInfo(assistant, AlarmAudit.class);
         TableInfoHelper.initTableInfo(assistant, WorkOrder.class);
         TableInfoHelper.initTableInfo(assistant, EquipmentBase.class);
         TableInfoHelper.initTableInfo(assistant, EquipmentStatus.class);
@@ -51,7 +48,6 @@ class AlarmClearServiceTest {
 
     private AlarmClearServiceImpl service;
     private AlarmDataService alarmDataService;
-    private AlarmAuditMapper alarmAuditMapper;
     private WorkOrderMapper workOrderMapper;
     private EquipmentBaseMapper equipmentBaseMapper;
     private EquipmentStatusMapper equipmentStatusMapper;
@@ -61,16 +57,14 @@ class AlarmClearServiceTest {
     @BeforeEach
     void setUp() {
         alarmDataService = mock(AlarmDataService.class);
-        alarmAuditMapper = mock(AlarmAuditMapper.class);
         workOrderMapper = mock(WorkOrderMapper.class);
         equipmentBaseMapper = mock(EquipmentBaseMapper.class);
         equipmentStatusMapper = mock(EquipmentStatusMapper.class);
         waterSupplyQueryService = mock(com.zksy.base.service.impl.WaterSupplyQueryService.class);
-        service = new AlarmClearServiceImpl(alarmDataService, alarmAuditMapper, workOrderMapper,
+        service = new AlarmClearServiceImpl(alarmDataService, workOrderMapper,
                 equipmentBaseMapper, equipmentStatusMapper, waterSupplyQueryService);
 
         when(equipmentBaseMapper.selectList(any())).thenReturn(Collections.singletonList(device()));
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
     }
 
@@ -107,10 +101,6 @@ class AlarmClearServiceTest {
         // 唯一报警已被误报解除并处理完毕,且不存在在办工单
         when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(Collections.singletonList(
                 alarm("alarm-1", "压力过低", 1)));
-        AlarmAudit released = new AlarmAudit();
-        released.setAlarmId("alarm-1");
-        released.setAuditResult(2);
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.singletonList(released));
 
         AlarmClearResult result = service.review("CN-GS-202609020001", "误报解除");
 
@@ -132,10 +122,6 @@ class AlarmClearServiceTest {
         when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(Arrays.asList(
                 alarm("alarm-1", "压力过低", 1),
                 alarm("alarm-2", "流量异常", 0)));
-        AlarmAudit released = new AlarmAudit();
-        released.setAlarmId("alarm-1");
-        released.setAuditResult(2);
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.singletonList(released));
 
         AlarmClearResult result = service.review("CN-GS-202609020001", "误报解除");
 
@@ -226,7 +212,6 @@ class AlarmClearServiceTest {
         // 新报警尚未派单:没有任何处置动作,不得立刻清警
         when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(Collections.singletonList(
                 alarm("alarm-new", "压力过低", 0)));
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
 
         AlarmClearResult result = service.review("CN-GS-202609020001", "误报解除");
@@ -266,7 +251,6 @@ class AlarmClearServiceTest {
         // 若按“没有工单就不算处置”处理,历史遗留行会把设备永久钉在报警态。
         when(alarmDataService.list(any(AbstractWrapper.class))).thenReturn(Collections.singletonList(
                 alarm("alarm-1", "压力过低", 1)));
-        when(alarmAuditMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(workOrderMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
 
         AlarmClearResult result = service.review("CN-GS-202609020001", "手工复核");

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

@@ -0,0 +1,31 @@
+package com.zksy.base.alarm.service;
+
+import com.zksy.common.core.redis.RedisCache;
+import com.zksy.common.exception.ServiceException;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockMultipartFile;
+import java.util.Arrays;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+class AlarmReviewAttachmentServiceTest {
+    private final RedisCache redis = mock(RedisCache.class);
+    private final AlarmReviewAttachmentService service = new AlarmReviewAttachmentService(redis);
+    @Test void rejectsEmptyAndDisguisedExecutableBeforeSaving() {
+        assertThrows(ServiceException.class, () -> service.upload("a1", new MockMultipartFile("file",new byte[0])));
+        assertThrows(ServiceException.class, () -> service.upload("a1",new MockMultipartFile("file","image.png","image/png","<script>test</script>".getBytes())));
+        assertThrows(ServiceException.class, () -> service.upload("a1",new MockMultipartFile("file","payload.svg","image/svg+xml","<svg/>".getBytes())));
+        verifyNoInteractions(redis);
+    }
+    @Test void rejectsPdfEvenWithAValidPdfHeaderOrRenamedAsImage() {
+        byte[] pdf = "%PDF-1.7 test".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+        assertThrows(ServiceException.class, () -> service.upload("a1", new MockMultipartFile("file","report.pdf","application/pdf",pdf)));
+        assertThrows(ServiceException.class, () -> service.upload("a1", new MockMultipartFile("file","report.png","image/png",pdf)));
+        verifyNoInteractions(redis);
+    }
+    @Test void rejectsDuplicateOrForgedAttachmentTickets() {
+        assertThrows(ServiceException.class, () -> service.resolve("a1",Arrays.asList("same","same")));
+        assertThrows(ServiceException.class, () -> service.resolve("a1",Arrays.asList("https://attacker.example/test.pdf")));
+        verifyNoInteractions(redis);
+    }
+}

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

@@ -0,0 +1,47 @@
+package com.zksy.base.alarm.service;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.zksy.base.alarm.dto.AlarmReviewQuery;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.mapping.BoundSql;
+import org.junit.jupiter.api.*;
+import java.io.InputStream;
+import java.util.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+class AlarmReviewMapperTest {
+    private final MybatisConfiguration configuration = new MybatisConfiguration();
+    @BeforeEach void parseMapper() throws Exception {
+        try (InputStream xml = getClass().getResourceAsStream("/mapper/base/AlarmReviewMapper.xml")) {
+            assertNotNull(xml);
+            new XMLMapperBuilder(xml, configuration, "AlarmReviewMapper.xml", configuration.getSqlFragments()).parse();
+        }
+    }
+    private BoundSql sql(String method, List<Integer> types, AlarmReviewQuery query) {
+        Map<String,Object> parameters = new HashMap<>();
+        parameters.put("query",query); parameters.put("typeIds",types);
+        parameters.put("unhandled",0); parameters.put("unreviewed",0);
+        return configuration.getMappedStatement("com.zksy.base.alarm.mapper.AlarmReviewMapper." + method).getBoundSql(parameters);
+    }
+    @Test void listFiltersUnhandledAndExactWarningCodeWithoutDeduplicatingDevices() {
+        AlarmReviewQuery query = new AlarmReviewQuery(); query.setWarningCode("WARN-GAS-CONCENTRATION"); query.setAuditStatus(0);
+        BoundSql bound = sql("selectPage",Arrays.asList(3,22),query);
+        String text = bound.getSql().replaceAll("\\s+"," ");
+        assertTrue(text.contains("a.alarm_status = ?")); assertTrue(text.contains("a.audit_status = ?"));
+        assertTrue(text.contains("a.warning_code = ?")); assertTrue(text.contains("e.equipment_type_id IN"));
+        assertTrue(text.contains("ORDER BY a.alarm_time DESC"));
+        assertFalse(text.contains("DISTINCT ON")); assertFalse(text.contains("GROUP BY"));
+        assertTrue(bound.getParameterMappings().stream().anyMatch(p -> "query.warningCode".equals(p.getProperty())));
+    }
+    @Test void emptyScopeNeverReturnsAllSystems() {
+        assertTrue(sql("selectPage",Collections.emptyList(),new AlarmReviewQuery()).getSql().contains("AND 1 = 0"));
+    }
+    @Test void statisticsSharesDeviceAndTimeFiltersButIgnoresSelectedAuditResult() {
+        AlarmReviewQuery query = new AlarmReviewQuery(); query.setAuditStatus(1); query.setKeyword("压力");
+        query.setStartTime(java.time.LocalDateTime.now()); query.setAscending(true);
+        String count = sql("selectStatistics",Arrays.asList(1,12),query).getSql().replaceAll("\\s+"," ");
+        assertTrue(count.contains("a.alarm_status = ?")); assertTrue(count.contains("a.alarm_time >= ?"));
+        assertTrue(count.contains("equipment_name ILIKE")); assertFalse(count.contains("AND a.audit_status"));
+        assertTrue(sql("selectPage",Arrays.asList(1,12),query).getSql().replaceAll("\\s+"," ").contains("ORDER BY a.alarm_time ASC"));
+    }
+}

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

@@ -0,0 +1,41 @@
+package com.zksy.base.alarm.service;
+
+import com.zksy.base.alarm.dto.AlarmReviewQuery;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+import com.zksy.common.core.domain.entity.SysMenu;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.core.domain.model.LoginUser;
+import com.zksy.system.service.ISysMenuService;
+import org.junit.jupiter.api.*;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import java.util.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class AlarmReviewScopeServiceTest {
+    private final EquipmentTypeMapper types = mock(EquipmentTypeMapper.class);
+    private final ISysMenuService menus = mock(ISysMenuService.class);
+    private final AlarmReviewScopeService service = new AlarmReviewScopeService(types, menus);
+    private final AlarmReviewQuery query = new AlarmReviewQuery();
+    @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");
+        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");
+        when(types.selectList(null)).thenReturn(Arrays.asList(type(1,0), type(3,0), type(22,3), type(99,22), type(12,1)));
+    }
+    @AfterEach void logout() { SecurityContextHolder.clearContext(); }
+    private EquipmentType type(int id, int parent) { EquipmentType t = new EquipmentType(); t.setId(id); t.setParentId(parent); return t; }
+    @Test void scopeIncludesRootAndAllDescendantsOnly() { assertEquals(Arrays.asList(3,22,99), service.resolve(query)); }
+    @Test void tamperedRootCannotAccessOtherSubsystem() { query.setRootTypeId(1); assertThrows(AccessDeniedException.class, () -> service.resolve(query)); }
+    @Test void gasCodeCannotBeOmittedOrReplaced() {
+        query.setWarningCode(null); assertThrows(AccessDeniedException.class, () -> service.resolve(query));
+        query.setWarningCode("WARN-GAS-PRESSURE"); assertThrows(AccessDeniedException.class, () -> service.resolve(query));
+    }
+    @Test void missingRootDoesNotFallBackToUnrestrictedList() { when(types.selectList(null)).thenReturn(Collections.singletonList(type(22,3))); assertTrue(service.resolve(query).isEmpty()); }
+}

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

@@ -0,0 +1,118 @@
+package com.zksy.base.alarm.service;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.zksy.base.alarm.domain.*;
+import com.zksy.base.alarm.dto.*;
+import com.zksy.base.alarm.enums.*;
+import com.zksy.base.alarm.mapper.*;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.core.domain.model.LoginUser;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.ops.service.WorkOrderService;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.*;
+import org.mockito.ArgumentCaptor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+class AlarmReviewServiceTest {
+    private final AlarmReviewMapper queries = mock(AlarmReviewMapper.class);
+    private final AlarmDataMapper alarms = mock(AlarmDataMapper.class);
+    private final AlarmLogMapper logs = mock(AlarmLogMapper.class);
+    private final AlarmReviewScopeService scope = mock(AlarmReviewScopeService.class);
+    private final AlarmReviewAttachmentService attachments = mock(AlarmReviewAttachmentService.class);
+    private final WorkOrderService orders = mock(WorkOrderService.class);
+    private final AlarmReviewService service = new AlarmReviewService(queries, alarms, logs, scope, attachments, orders);
+    private final AlarmReviewQuery query = new AlarmReviewQuery();
+    private AlarmReviewVO alarm;
+
+    @BeforeAll static void tables() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, AlarmData.class);
+        TableInfoHelper.initTableInfo(assistant, AlarmLog.class);
+    }
+    @BeforeEach void setup() {
+        SysUser user = new SysUser(); user.setUserName("审核员");
+        LoginUser principal = new LoginUser(9L, 10L, user, Collections.emptySet());
+        SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(principal, null));
+        query.setRootTypeId(1);
+        when(scope.resolve(query)).thenReturn(Arrays.asList(1, 12));
+        alarm = new AlarmReviewVO(); alarm.setId("a1"); alarm.setAlarmLevel(1);
+        alarm.setAlarmStatus(AlarmRecordStatusEnum.UNHANDLED.getCode());
+        alarm.setAuditStatus(AlarmAuditStatusEnum.UNREVIEWED.getCode());
+        when(queries.selectDetail(eq("a1"), eq(query), anyList())).thenReturn(alarm);
+        when(alarms.update(isNull(), any())).thenReturn(1);
+        when(logs.insert(any(AlarmLog.class))).thenReturn(1);
+    }
+    @AfterEach void logout() { SecurityContextHolder.clearContext(); }
+    private AlarmReviewRequest request(AlarmAuditStatusEnum result) {
+        AlarmReviewRequest request = new AlarmReviewRequest();
+        request.setAuditStatus(result.getCode()); request.setOpinion("  现场核实  ");
+        return request;
+    }
+
+    @Test void confirmationCreatesOrderWithoutHandlingAlarmOrChangingDeviceState() {
+        when(orders.createWorkOrderFromAlarm("a1", null, null)).thenReturn(88L);
+        AlarmReviewVO result = service.review("a1", query, request(AlarmAuditStatusEnum.CONFIRMED));
+        assertEquals(88L, result.getOrderId()); assertEquals(0, result.getAlarmStatus());
+        assertEquals("一级报警", result.getAlarmLevelName()); assertEquals("确认报警", result.getAuditStatusName());
+        ArgumentCaptor<AlarmLog> capture = ArgumentCaptor.forClass(AlarmLog.class);
+        verify(logs).insert(capture.capture());
+        assertEquals(9L, capture.getValue().getOperUserId());
+        assertEquals("现场核实", capture.getValue().getOperDesc());
+        // 不把审核意见作为故障描述传入,委托工单服务根据原报警生成 order_desc。
+        verify(orders).createWorkOrderFromAlarm("a1", null, null);
+        ArgumentCaptor<LambdaUpdateWrapper<AlarmData>> update = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
+        verify(alarms).update(isNull(), update.capture());
+        String where = update.getValue().getSqlSegment();
+        assertTrue(where.contains("alarm_status")); assertTrue(where.contains("audit_status"));
+        assertFalse(update.getValue().getSqlSet().contains("alarm_status"));
+        // 审核服务无设备状态写依赖;只允许这一条报警更新。
+        verifyNoMoreInteractions(alarms);
+    }
+    @Test void falseAlarmRecordsOpinionAndAttachmentsWithoutCreatingOrder() {
+        AlarmReviewRequest request = request(AlarmAuditStatusEnum.FALSE_ALARM);
+        request.setAttachmentTokens(Collections.singletonList("ticket"));
+        when(attachments.resolve("a1", request.getAttachmentTokens())).thenReturn("/profile/test.pdf");
+        AlarmReviewVO result = service.review("a1", query, request);
+        assertEquals(0, result.getAlarmStatus()); assertEquals(2, result.getAuditStatus());
+        verifyNoInteractions(orders);
+        ArgumentCaptor<AlarmLog> log = ArgumentCaptor.forClass(AlarmLog.class);
+        verify(logs).insert(log.capture()); assertEquals("/profile/test.pdf", log.getValue().getAttachUrl());
+    }
+    @Test void concurrentOrRepeatedReviewWritesNeitherLogNorOrder() {
+        when(alarms.update(isNull(), any())).thenReturn(0);
+        assertThrows(ServiceException.class, () -> service.review("a1", query, request(AlarmAuditStatusEnum.CONFIRMED)));
+        verifyNoInteractions(logs, orders);
+    }
+    @Test void blankOpinionAndUnreviewedResultAreRejectedBeforeWrites() {
+        AlarmReviewRequest request = request(AlarmAuditStatusEnum.CONFIRMED); request.setOpinion("  ");
+        assertThrows(ServiceException.class, () -> service.review("a1", query, request));
+        assertThrows(ServiceException.class, () -> service.review("a1", query, request(AlarmAuditStatusEnum.UNREVIEWED)));
+        verifyNoInteractions(alarms, logs, orders);
+    }
+    @Test void workOrderFailurePropagatesAndReviewHasRollbackBoundary() throws Exception {
+        when(orders.createWorkOrderFromAlarm(anyString(), isNull(), isNull())).thenThrow(new ServiceException("建单失败"));
+        assertThrows(ServiceException.class, () -> service.review("a1", query, request(AlarmAuditStatusEnum.CONFIRMED)));
+        Transactional tx = AlarmReviewService.class.getMethod("review", String.class, AlarmReviewQuery.class, AlarmReviewRequest.class).getAnnotation(Transactional.class);
+        assertNotNull(tx); assertTrue(Arrays.asList(tx.rollbackFor()).contains(Exception.class));
+    }
+    @Test void invalidPageLevelAndDateRangeAreRejected() {
+        query.setPageSize(101); assertThrows(ServiceException.class, () -> service.page(query));
+        query.setPageSize(10); query.setAlarmLevel(5); assertThrows(ServiceException.class, () -> service.page(query));
+        query.setAlarmLevel(null); query.setStartTime(java.time.LocalDateTime.now()); query.setEndTime(query.getStartTime().minusDays(1));
+        assertThrows(ServiceException.class, () -> service.page(query)); verifyNoInteractions(queries);
+    }
+    @Test void outOfScopeDetailIsRejectedBeforeAnyWrite() {
+        when(queries.selectDetail(eq("a1"), eq(query), anyList())).thenReturn(null);
+        assertThrows(ServiceException.class, () -> service.review("a1", query, request(AlarmAuditStatusEnum.CONFIRMED)));
+        verifyNoInteractions(alarms, logs, orders);
+    }
+}

+ 58 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/ops/service/impl/WorkOrderServiceImplTest.java

@@ -115,6 +115,64 @@ class WorkOrderServiceImplTest {
         ReflectionTestUtils.setField(service, "assignScopeSupport", mock(OpsAssignScopeSupport.class));
     }
 
+    @Test
+    void alarmConfirmationLinksNewOrderWithoutHandlingAlarmOrClearingDevice() {
+        AlarmData alarm = new AlarmData();
+        alarm.setId("a-new"); alarm.setDeviceCode("DEVICE-NEW"); alarm.setAlarmLevel(1);
+        alarm.setAlarmStatus(0); alarm.setAuditStatus(1);
+        when(alarmDataService.getById("a-new")).thenReturn(alarm);
+        EquipmentBase device = new EquipmentBase(); device.setEquipmentId("eq-new");
+        when(equipmentBaseMapper.selectOne(any())).thenReturn(device);
+        when(orderMapper.insert(any(WorkOrder.class))).thenAnswer(call -> {
+            WorkOrder order = call.getArgument(0); order.setOrderId(99L); return 1;
+        });
+        when(alarmDataService.update(any(AlarmData.class), any())).thenReturn(true);
+        assertEquals(99L, service.createWorkOrderFromAlarm("a-new", null, "现场确认"));
+        org.mockito.ArgumentCaptor<AlarmData> relation = org.mockito.ArgumentCaptor.forClass(AlarmData.class);
+        verify(alarmDataService).update(relation.capture(), any());
+        assertEquals(99L, relation.getValue().getOrderId());
+        assertNull(relation.getValue().getAlarmStatus());
+        assertNull(relation.getValue().getHandleTime());
+        assertNull(relation.getValue().getAuditStatus());
+        org.mockito.Mockito.verifyNoInteractions(alarmClearService);
+        verify(caseInfoMapper).update(isNull(), any());
+    }
+
+    @Test
+    void alarmOrderPersistsFaultDescriptionWithZeroActualValue() {
+        AlarmData alarm = new AlarmData();
+        alarm.setWarningType("压力预警");
+        alarm.setActualValue(java.math.BigDecimal.ZERO);
+        alarm.setMinValue(java.math.BigDecimal.ONE);
+        alarm.setMaxValue(java.math.BigDecimal.TEN);
+        WorkOrder order = createAlarmOrderWithGeneratedDescription(alarm);
+        assertEquals("压力预警,当前值:0(阈值:1~10),请现场核实处理", order.getOrderDesc());
+    }
+
+    @Test
+    void alarmOrderWithoutActualValueOmitsPlaceholderAndThreshold() {
+        AlarmData alarm = new AlarmData();
+        alarm.setWarningType("设备离线");
+        alarm.setMaxValue(java.math.BigDecimal.ZERO);
+        WorkOrder order = createAlarmOrderWithGeneratedDescription(alarm);
+        assertEquals("设备离线,请现场核实处理", order.getOrderDesc());
+    }
+
+    private WorkOrder createAlarmOrderWithGeneratedDescription(AlarmData alarm) {
+        alarm.setId("a-description"); alarm.setDeviceCode("DEVICE-DESC"); alarm.setAlarmLevel(1);
+        when(alarmDataService.getById("a-description")).thenReturn(alarm);
+        EquipmentBase device = new EquipmentBase(); device.setEquipmentId("eq-description");
+        when(equipmentBaseMapper.selectOne(any())).thenReturn(device);
+        when(orderMapper.insert(any(WorkOrder.class))).thenAnswer(call -> {
+            WorkOrder order = call.getArgument(0); order.setOrderId(100L); return 1;
+        });
+        when(alarmDataService.update(any(AlarmData.class), any())).thenReturn(true);
+        assertEquals(100L, service.createWorkOrderFromAlarm("a-description", null, null));
+        org.mockito.ArgumentCaptor<WorkOrder> saved = org.mockito.ArgumentCaptor.forClass(WorkOrder.class);
+        verify(orderMapper).insert(saved.capture());
+        return saved.getValue();
+    }
+
     @Test
     void dispatchRejectsMissingHandlerId() {
         ServiceException error = assertThrows(ServiceException.class,

+ 4 - 0
zk-api-service/src/main/java/com/zksy/api/domain/AlarmData.java

@@ -62,6 +62,10 @@ public class AlarmData implements Serializable {
     @ApiModelProperty(value = "报警状态 0-未处理 1-已处理")
     private Integer alarmStatus;
 
+    @TableField(value = "audit_status")
+    @ApiModelProperty(value = "审核结果:0-未审核 1-确认报警 2-误报警;由管理端统一审核")
+    private Integer auditStatus;
+
     @TableField(value = "alarm_time")
     @ApiModelProperty(value = "报警时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

+ 6 - 56
zk-api-service/src/main/java/com/zksy/api/service/impl/AlarmDataServiceImpl.java

@@ -6,8 +6,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.api.domain.*;
 import com.zksy.api.enums.EquipmentTypeEnum;
-import com.zksy.api.enums.WorkOrderStatusEnum;
-import com.zksy.api.enums.WorkOrderLogOperTypeEnum;
 import com.zksy.api.mapper.*;
 import com.zksy.api.service.AlarmDataService;
 import com.zksy.api.service.CollectionAlarmWriter;
@@ -21,8 +19,6 @@ import org.springframework.transaction.support.TransactionTemplate;
 import java.time.LocalDateTime;
 import java.math.BigDecimal;
 import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Random;
 
 @Slf4j
@@ -35,12 +31,6 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
     @Autowired
     private CaseInfoApiMapper caseInfoApiMapper;
 
-    @Autowired
-    private WorkOrderApiMapper workOrderApiMapper;
-
-    @Autowired
-    private WorkOrderLogApiMapper workOrderLogApiMapper;
-
     @Autowired
     private PlatformTransactionManager transactionManager;
 
@@ -52,10 +42,9 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
         boolean inserted = collectionAlarmWriter.saveOrUpdateUnhandled(alarmData);
         if (inserted) {
             try {
-                new TransactionTemplate(transactionManager).executeWithoutResult(status -> syncCreateCaseAndWorkOrder(alarmData));
+                new TransactionTemplate(transactionManager).executeWithoutResult(status -> syncCreateCase(alarmData));
             } catch (Exception e) {
-                alarmData.setOrderId(null);
-                log.error("保存告警后同步创建案件/工单失败,alarmId={}", alarmData.getId(), e);
+                log.error("保存告警后同步创建案件失败,alarmId={}", alarmData.getId(), e);
             }
         }
         // 复用原告警(等级更新/同级跳过)也是正常完成,不影响后续短信、设备状态和采集。
@@ -79,10 +68,10 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
     }
 
     /**
-     * 根据设备类别ID同步创建案件和工单
+     * 根据设备类别ID同步创建案件
      */
-    private void syncCreateCaseAndWorkOrder(AlarmData alarmData) {
-        log.info("保存设备告警后同步创建案件和工单-入参:{}", alarmData);
+    private void syncCreateCase(AlarmData alarmData) {
+        log.info("保存设备告警后同步创建案件-入参:{}", alarmData);
         // 根据设备编码查询设备基础信息
         EquipmentBase equipmentBase = equipmentBaseApiMapper.selectOne(new LambdaQueryWrapper<EquipmentBase>().eq(EquipmentBase::getEquipmentCode, alarmData.getDeviceCode()).last("LIMIT 1"));
         if (equipmentBase == null) {
@@ -92,9 +81,7 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
 
         CaseInfo caseInfo = null;
 
-        // 工单优先级与管理端一致:0-紧急、1-一般、2-低。
         int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : 4;
-        int orderLevel = caseLevel <= 2 ? 0 : (caseLevel == 3 ? 1 : 2);
 
         // 构建告警描述
         String desc = alarmData.getWarningType() != null ? alarmData.getWarningType() : "设备告警";
@@ -116,33 +103,6 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
             log.info("同步新增案件信息成功,caseNo={}, alarmId={}", caseInfo.getCaseNo(), alarmData.getId());
         }
 
-        // 同步新增工单
-        WorkOrder workOrder = new WorkOrder();
-        workOrder.setOrderNo(generateOrderNo());
-        workOrder.setDeviceId(equipmentBase.getEquipmentId());
-        workOrder.setOrderLevel(orderLevel);
-        workOrder.setOrderDesc(desc);
-        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
-        requireSaved(workOrderApiMapper.insert(workOrder) > 0);
-        AlarmData relation = new AlarmData();
-        relation.setId(alarmData.getId());
-        relation.setOrderId(workOrder.getOrderId());
-        requireSaved(baseMapper.update(relation, new LambdaQueryWrapper<AlarmData>().eq(AlarmData::getId, alarmData.getId()).isNull(AlarmData::getOrderId)) > 0);
-        alarmData.setOrderId(workOrder.getOrderId());
-        if (caseInfo != null) {
-            caseInfo.setOrderId(workOrder.getOrderId());
-            requireSaved(caseInfoApiMapper.updateById(caseInfo) > 0);
-        }
-        log.info("同步新增工单成功,orderNo={}, alarmId={}", workOrder.getOrderNo(), alarmData.getId());
-
-        // 新增工单操作日志
-        WorkOrderLog workOrderLog = new WorkOrderLog();
-        workOrderLog.setOrderId(workOrder.getOrderId());
-        workOrderLog.setOperType(WorkOrderLogOperTypeEnum.CREATE.getCode());
-        workOrderLog.setOperDesc(desc + ",系统告警自动创建工单");
-        workOrderLog.setOperUserName("系统");
-        requireSaved(workOrderLogApiMapper.insert(workOrderLog) > 0);
-        log.info("同步新增工单操作日志成功,orderId={}, alarmId={}", workOrder.getOrderId(), alarmData.getId());
     }
 
     /**
@@ -157,15 +117,5 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
         return "CS" + currentDateTime + randomNum;
     }
 
-    /**
-     * 生成工单编号:WO + 年月日时分秒 + 4位随机数
-     */
-    public static String generateOrderNo() {
-        // 1. 获取当前时间,格式化为14位年月日时分秒
-        String currentDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
-        // 2. 生成4位随机数,不足补0
-        String randomNum = String.format("%04d", new Random().nextInt(10000));
-        // 3. 拼接完整编号
-        return "WO" + currentDateTime + randomNum;
-    }
+
 }