فهرست منبع

Merge remote-tracking branch 'origin/master'

Kazerin 17 ساعت پیش
والد
کامیت
943f0aaebc
62فایلهای تغییر یافته به همراه1644 افزوده شده و 701 حذف شده
  1. 14 12
      pipe-network-service/sql/dispatch_refactor.sql
  2. 3 0
      pipe-network-service/sql/dispatch_refactor_migrate.sql
  3. 32 16
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventController.java
  4. 11 13
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisPlanController.java
  5. 62 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainagePipeNetworkController.java
  6. 31 13
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/EngineeringFacilityController.java
  7. 13 110
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/PipeNetworkInfoController.java
  8. 38 18
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/WaterloggingPointController.java
  9. 160 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/mobile/MobileEmergencyController.java
  10. 17 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/system/SysLoginController.java
  11. 4 4
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureBaseArchivesAdapterTest.java
  12. 4 5
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureOperatingInformationAdapterTest.java
  13. 5 5
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitFacilityCountCrossEndpointContractTest.java
  14. 5 23
      pipe-network-service/zksy-framework/src/main/java/com/zksy/framework/web/service/SysPermissionService.java
  15. 1 1
      pipe-network-service/zksy-framework/src/main/java/com/zksy/framework/web/service/UserDetailsServiceImpl.java
  16. 11 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/PipeNetworkBase.java
  17. 20 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/dto/PipeNetworkPageInDTO.java
  18. 25 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/PipeNetworkBaseService.java
  19. 112 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/PipeNetworkBaseServiceImpl.java
  20. 6 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureBaseArchivesAdapter.java
  21. 9 7
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureOperatingInformationAdapter.java
  22. 3 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEvent.java
  23. 9 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEventLog.java
  24. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisPlan.java
  25. 3 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisDeptOptionVO.java
  26. 3 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventPageDTO.java
  27. 18 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventVO.java
  28. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisHomeVO.java
  29. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisUserOptionVO.java
  30. 23 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventAuditDTO.java
  31. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventReleaseDTO.java
  32. 0 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventUrgeRowVO.java
  33. 2 8
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventVerifyDTO.java
  34. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanApproveDTO.java
  35. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanPageDTO.java
  36. 8 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/enums/EventLogOperTypeEnum.java
  37. 13 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/enums/EventStatusEnum.java
  38. 2 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DispatchUserMapper.java
  39. 15 14
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventService.java
  40. 127 55
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventServiceImpl.java
  41. 16 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisPlanServiceImpl.java
  42. 23 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/support/DispatchAttachUrlSupport.java
  43. 64 16
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/EngineeringFacility.java
  44. 64 21
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/WaterloggingPoint.java
  45. 43 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/dto/EngineeringFacilityPageInDTO.java
  46. 37 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/dto/WaterloggingPointPageInDTO.java
  47. 8 7
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/mapper/DrainageVideoMapper.java
  48. 5 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageVideoService.java
  49. 13 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/EngineeringFacilityService.java
  50. 3 27
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/PipeNetworkInfoService.java
  51. 15 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/WaterloggingPointService.java
  52. 12 10
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageEquipmentBindingServiceImpl.java
  53. 64 14
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/EngineeringFacilityServiceImpl.java
  54. 3 67
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/PipeNetworkInfoServiceImpl.java
  55. 139 36
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/WaterloggingPointServiceImpl.java
  56. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/mapper/SysUserMapper.java
  57. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/ISysUserService.java
  58. 91 142
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/impl/SysUserServiceImpl.java
  59. 6 0
      pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml
  60. 46 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/EventStatusContractTest.java
  61. 119 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/EventVerifyWithoutDepartmentTest.java
  62. 31 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/support/DispatchAttachUrlSupportTest.java

+ 14 - 12
pipe-network-service/sql/dispatch_refactor.sql

@@ -12,8 +12,12 @@
 -- 设计口径(2026-10-14 与业务确认):
 --   · 事件等级:3 级 —— Ⅰ级(紧急) / Ⅱ级(较大) / Ⅲ级(一般)(原型口径)
 --   · 事件类别、事件等级分别使用 dis_event_category / dis_event_level
---   · 事件状态:5 态 —— 0 待核实 / 1 已核实 / 2 处置中 / 3 已解除 / 4 不实事件
---     存量「待受理」归 0、「已关闭」归 3
+--   · 事件状态:6 态 —— 0 待核实 / 1 已核实 / 2 处置中 / 3 待验收 / 4 已解除 / 5 不实事件
+--     存量数字旧编码须执行 migration_dispatch_event_status.sql;历史中文数据先执行
+--     dispatch_refactor_migrate.sql,再执行 migration_dispatch_event_status.sql。
+--     空库新建无需执行状态升级脚本。
+--   · 事件日志 oper_type 不设 CHECK;存量库须执行 migration_dispatch_event_log_oper_type.sql 删除旧约束。
+--     CREATE TABLE IF NOT EXISTS 不会删除已有日志表的 CHECK 约束。
 --
 -- 幂等性:本脚本可重复执行。
 --   建表用 CREATE TABLE IF NOT EXISTS;加列用 ADD COLUMN IF NOT EXISTS;
@@ -54,7 +58,6 @@ CREATE TABLE IF NOT EXISTS app_user.dis_event (
     verify_opinion   varchar(500),
     handler          varchar(64),
     handler_phone    varchar(20),
-    department       varchar(128),
     dispose_time     timestamp,
     release_time     timestamp,
     release_reason   varchar(20),
@@ -87,7 +90,6 @@ ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS verify_time     timestam
 ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS verify_opinion  varchar(500);
 ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS handler         varchar(64);
 ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS handler_phone   varchar(20);
-ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS department      varchar(128);
 ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS dispose_time    timestamp;
 ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS release_time    timestamp;
 ALTER TABLE app_user.dis_event ADD COLUMN IF NOT EXISTS release_reason  varchar(20);
@@ -114,16 +116,15 @@ COMMENT ON COLUMN app_user.dis_event.happen_time     IS '发生时间';
 COMMENT ON COLUMN app_user.dis_event.reporter_phone  IS '上报人联系电话';
 COMMENT ON COLUMN app_user.dis_event.event_category  IS '事件类别:字典 dis_event_category(管道泄漏/路面积水/设施损坏/管道堵塞)';
 COMMENT ON COLUMN app_user.dis_event.event_level     IS '事件等级:字典 dis_event_level(1-Ⅰ级紧急 2-Ⅱ级较大 3-Ⅲ级一般)';
-COMMENT ON COLUMN app_user.dis_event.status          IS '事件状态:EventStatusEnum —— 0-待核实 1-已核实 2-处置中 3-已解除 4-不实事件;终态为 3、4';
-COMMENT ON COLUMN app_user.dis_event.verify_result   IS '核实结论:0-真实 1-不实;仅 status in (1,4) 时有值';
+COMMENT ON COLUMN app_user.dis_event.status          IS '事件状态:EventStatusEnum —— 0-待核实 1-已核实 2-处置中 3-待验收 4-已解除 5-不实事件;终态为 4、5';
+COMMENT ON COLUMN app_user.dis_event.verify_result   IS '核实结论:0-真实 1-不实;初判核实后保留';
 COMMENT ON COLUMN app_user.dis_event.verifier        IS '核验人';
 COMMENT ON COLUMN app_user.dis_event.verify_time     IS '核实时间';
 COMMENT ON COLUMN app_user.dis_event.verify_opinion  IS '核实意见;判定为不实时填核销原因(字典 dis_event_false_reason)';
 COMMENT ON COLUMN app_user.dis_event.handler         IS '责任处置人(后续工作催办的催办对象)';
 COMMENT ON COLUMN app_user.dis_event.handler_phone   IS '责任处置人联系电话';
-COMMENT ON COLUMN app_user.dis_event.department      IS '责任单位 / 转接接收部门';
 COMMENT ON COLUMN app_user.dis_event.dispose_time    IS '开始处置时间(status 1→2 时写入)';
-COMMENT ON COLUMN app_user.dis_event.release_time    IS '解除时间(status 2→3 时写入)';
+COMMENT ON COLUMN app_user.dis_event.release_time    IS '解除时间(核验通过,status 3→4 时写入)';
 COMMENT ON COLUMN app_user.dis_event.release_reason  IS '解除原因:字典 dis_event_release_reason';
 COMMENT ON COLUMN app_user.dis_event.release_note    IS '解除说明 / 处置结果补充';
 COMMENT ON COLUMN app_user.dis_event.last_visit_time IS '最近回访时间(取最近一次处置跟踪的跟踪时间)';
@@ -132,6 +133,7 @@ COMMENT ON COLUMN app_user.dis_event.need_upgrade    IS '是否需要升级:
 COMMENT ON COLUMN app_user.dis_event.del_flag        IS '删除标志(0=正常 2=删除)';
 
 ALTER TABLE app_user.dis_event
+    DROP COLUMN IF EXISTS department,
     DROP COLUMN IF EXISTS reporter,
     DROP COLUMN IF EXISTS reporter_id,
     DROP COLUMN IF EXISTS report_time,
@@ -153,7 +155,7 @@ CREATE INDEX IF NOT EXISTS idx_dis_event_level          ON app_user.dis_event(ev
 -- ---------------------------------------------------------------------
 -- 1.2 dis_event_log 事件日志表
 --     一张表承载全部环节留痕:接报登记 / 初判核实 / 不实核销 / 电话转接 /
---     上报报送 / 开始处置 / 处置跟踪 / 事件解除 / 工作催办 / 信息修改。
+--     上报报送 / 开始处置 / 处置跟踪 / 事件解除 / 工作催办 / 信息修改 / 提交核验 / 最终核验。
 --     替代旧 dis_event_transfer_log + dis_event_track_log + dis_event_urge_detail。
 --     日志完全由业务代码写入,不使用触发器/自定义函数。
 -- ---------------------------------------------------------------------
@@ -161,7 +163,7 @@ CREATE TABLE IF NOT EXISTS app_user.dis_event_log (
     id              int8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     event_id        int8 NOT NULL,
     event_no        varchar(32),
-    oper_type       int2 NOT NULL CHECK (oper_type BETWEEN 0 AND 9),
+    oper_type       int2 NOT NULL,
     oper_desc       text,
     status_from     int2,
     status_to       int2,
@@ -199,7 +201,7 @@ ALTER TABLE app_user.dis_event_log ADD COLUMN IF NOT EXISTS oper_time       time
 COMMENT ON TABLE  app_user.dis_event_log IS '应急事件操作日志:一个事件 N 行,记录全部环节动作与状态变更轨迹;仅由业务代码写入';
 COMMENT ON COLUMN app_user.dis_event_log.event_id        IS '关联 dis_event.id';
 COMMENT ON COLUMN app_user.dis_event_log.event_no        IS '事件编号(冗余,便于按编号单独查日志)';
-COMMENT ON COLUMN app_user.dis_event_log.oper_type       IS 'EventLogOperTypeEnum:0-接报登记 1-初判核实 2-不实核销 3-电话转接 4-上报报送 5-开始处置 6-处置跟踪 7-事件解除 8-工作催办 9-信息修改';
+COMMENT ON COLUMN app_user.dis_event_log.oper_type       IS 'EventLogOperTypeEnum:0-接报登记 1-初判核实 2-不实核销 3-电话转接 4-上报报送 5-开始处置 6-处置跟踪 7-事件解除 8-工作催办 9-信息修改 10-提交核验 11-最终核验';
 COMMENT ON COLUMN app_user.dis_event_log.oper_desc       IS '操作说明/备注:跟踪内容、催办内容、核实意见、解除说明等';
 COMMENT ON COLUMN app_user.dis_event_log.status_from     IS '变更前事件状态;不改状态的动作为 NULL';
 COMMENT ON COLUMN app_user.dis_event_log.status_to       IS '变更后事件状态;不改状态的动作为 NULL';
@@ -594,7 +596,7 @@ COMMIT;
 -- 2) 字典是否写入
 --    SELECT dict_type, count(*) FROM app_user.sys_dict_data
 --     WHERE dict_type LIKE 'dis_%' GROUP BY dict_type ORDER BY dict_type;
--- 3) 事件状态分布是否符合预期(只应是 0~4)
+-- 3) 事件状态分布是否符合预期(只应是 0~5)
 --    SELECT status, count(*) FROM app_user.dis_event GROUP BY status ORDER BY status;
 -- 4) 日志是否迁移到位
 --    SELECT oper_type, count(*) FROM app_user.dis_event_log GROUP BY oper_type ORDER BY oper_type;

+ 3 - 0
pipe-network-service/sql/dispatch_refactor_migrate.sql

@@ -1,3 +1,6 @@
+-- 历史迁移保留旧数字编码(3已解除/4不实事件/5待验收)。
+-- 完成本脚本后,部署新版应用前执行 migration_dispatch_event_status.sql。
+-- 已升级为新版数字编码的数据库不得重跑本历史迁移。
 -- =====================================================================
 -- 应急与指挥调度模块(dispatch)改造脚本 —— 存量数据搬迁
 -- 数据库:KingbaseES(PG 兼容模式),业务 schema = app_user

+ 32 - 16
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventController.java

@@ -135,11 +135,18 @@ public class DisEventController {
 
     // ==================== 初判核实 ====================
 
-    @GetMapping("/userOptions")
-    @ApiOperation("责任处置人下拉项(启用状态的系统用户:昵称 + 联系电话 + 所属部门)")
-    @Anonymous
-    public AjaxResult userOptions() {
-        return AjaxResult.success(service.listUserOptions());
+    @GetMapping("/handlerOptions")
+    @ApiOperation("处置人下拉项(具备处置权限的启用系统用户)")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:verify')")
+    public AjaxResult handlerOptions() {
+        return AjaxResult.success(service.listHandlerOptions());
+    }
+
+    @GetMapping("/verifierOptions")
+    @ApiOperation("核验人下拉项(具备核验权限的启用系统用户)")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:verify')")
+    public AjaxResult verifierOptions() {
+        return AjaxResult.success(service.listVerifierOptions());
     }
 
     @GetMapping("/deptOptions")
@@ -170,7 +177,7 @@ public class DisEventController {
     @PostMapping("/transfer")
     @ApiOperation("电话转接(写日志,不改事件状态)")
     @Log(title = "转接事件", businessType = BusinessType.UPDATE)
-    @PreAuthorize("@ss.hasPermi('dispatch:event:transfer')")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:dispatch')")
     public AjaxResult transfer(@RequestBody EventTransferDTO dto) {
         service.transfer(dto);
         return AjaxResult.success("转接成功");
@@ -179,7 +186,7 @@ public class DisEventController {
     @PostMapping("/report")
     @ApiOperation("上报报送(写日志,不改事件状态)")
     @Log(title = "报送事件", businessType = BusinessType.UPDATE)
-    @PreAuthorize("@ss.hasPermi('dispatch:event:report')")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:dispatch')")
     public AjaxResult report(@RequestBody EventTransferDTO dto) {
         service.report(dto);
         return AjaxResult.success("报送成功");
@@ -190,7 +197,7 @@ public class DisEventController {
     @PostMapping("/startDispose")
     @ApiOperation("开始处置(已核实 → 处置中)")
     @Log(title = "开始处置", businessType = BusinessType.UPDATE)
-    @PreAuthorize("@ss.hasPermi('dispatch:event:dispose')")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:handle')")
     public AjaxResult startDispose(@RequestBody EventTrackDTO dto) {
         service.startDispose(dto);
         return AjaxResult.success("已开始处置");
@@ -199,19 +206,28 @@ public class DisEventController {
     @PostMapping("/track")
     @ApiOperation("处置跟踪(可多次记录,不改事件状态)")
     @Log(title = "处置跟踪", businessType = BusinessType.INSERT)
-    @PreAuthorize("@ss.hasPermi('dispatch:event:track')")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:handle')")
     public AjaxResult track(@RequestBody EventTrackDTO dto) {
         service.track(dto);
         return AjaxResult.success("跟踪记录保存成功");
     }
 
-    @PostMapping("/release/{id}")
-    @ApiOperation("事件解除(处置中 → 已解除)")
-    @Log(title = "解除事件", businessType = BusinessType.UPDATE)
-    @PreAuthorize("@ss.hasPermi('dispatch:event:release')")
-    public AjaxResult release(@PathVariable Long id, @RequestBody EventReleaseDTO dto) {
-        service.release(id, dto);
-        return AjaxResult.success("事件已解除");
+    @PostMapping("/submitVerify")
+    @ApiOperation("处置人提交核验(处置中 → 待验收)")
+    @Log(title = "提交事件核验", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('dispatch:event:handle')")
+    public AjaxResult submitVerify(@RequestBody EventTrackDTO dto) {
+        service.submitVerify(dto);
+        return AjaxResult.success("已提交核验");
+    }
+
+    @PostMapping("/audit/{id}")
+    @ApiOperation("指定核验人最终核验(已解除 / 退回处置中)")
+    @Log(title = "核验事件处置", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('dispatch:event:audit')")
+    public AjaxResult audit(@PathVariable Long id, @RequestBody EventAuditDTO dto) {
+        service.audit(id, dto);
+        return AjaxResult.success("核验完成");
     }
 
     // ==================== 工作催办 ====================

+ 11 - 13
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisPlanController.java

@@ -126,20 +126,18 @@ public class DisPlanController {
         return AjaxResult.success("提交审核成功");
     }
 
-    @PostMapping("/approve/{id}")
-    @ApiOperation("审批通过(候选版本生效,旧版本降为历史)")
-    @Log(title = "审批通过预案", businessType = BusinessType.UPDATE)
+    @PostMapping("/review/{id}")
+    @ApiOperation("预案审核(通过或驳回)")
+    @Log(title = "审核预案", businessType = BusinessType.UPDATE)
     @PreAuthorize("@ss.hasPermi('dispatch:plan:approve')")
-    public AjaxResult approve(@PathVariable Long id, @RequestBody(required = false) PlanApproveDTO dto) {
-        service.approve(id, dto == null ? new PlanApproveDTO() : dto);
-        return AjaxResult.success("审批通过");
-    }
-
-    @PostMapping("/reject/{id}")
-    @ApiOperation("审批不通过(候选版本驳回,原生效版本仍生效)")
-    @Log(title = "驳回预案", businessType = BusinessType.UPDATE)
-    @PreAuthorize("@ss.hasPermi('dispatch:plan:approve')")
-    public AjaxResult reject(@PathVariable Long id, @RequestBody PlanApproveDTO dto) {
+    public AjaxResult review(@PathVariable Long id, @RequestBody PlanApproveDTO dto) {
+        if (dto.getApproved() == null) {
+            return AjaxResult.error("请选择审核结果");
+        }
+        if (dto.getApproved()) {
+            service.approve(id, dto);
+            return AjaxResult.success("审批通过");
+        }
         service.reject(id, dto);
         return AjaxResult.success("已驳回");
     }

+ 62 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainagePipeNetworkController.java

@@ -0,0 +1,62 @@
+package com.zksy.web.controller.drainage;
+
+import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.dto.PipeNetworkPageInDTO;
+import com.zksy.base.service.PipeNetworkBaseService;
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.core.domain.AjaxResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 排水管网基础数据管理
+ * <p>数据源统一为 pipe_network_base,且强制 network_type=1(排水设施)。</p>
+ * <p>排水管网数据由外部导入维护,本模块只提供查询能力:分页列表、地图数据、详情。</p>
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/drainage/pipeNetworkBase")
+@Api(tags = "排水管网基础数据管理")
+public class DrainagePipeNetworkController {
+
+    @Autowired
+    private PipeNetworkBaseService pipeNetworkBaseService;
+
+    @Anonymous
+    @GetMapping("/page")
+    @ApiOperation(value = "排水管网分页查询")
+    public AjaxResult page(PipeNetworkPageInDTO dto) {
+        return AjaxResult.success(pipeNetworkBaseService.queryDrainageByPage(dto));
+    }
+
+    @Anonymous
+    @GetMapping("/map")
+    @ApiOperation(value = "排水管网地图数据(不分页,按筛选条件返回带坐标的记录)")
+    public AjaxResult map(PipeNetworkPageInDTO dto) {
+        return AjaxResult.success(pipeNetworkBaseService.listDrainageForMap(dto));
+    }
+
+    @Anonymous
+    @GetMapping("/filterOptions")
+    @ApiOperation(value = "排水管网筛选下拉项(材质、管养单位)")
+    public AjaxResult filterOptions() {
+        return AjaxResult.success(pipeNetworkBaseService.getDrainageFilterOptions());
+    }
+
+    @Anonymous
+    @GetMapping("/{id}")
+    @ApiOperation(value = "排水管网详情")
+    public AjaxResult getById(@PathVariable String id) {
+        PipeNetworkBase entity = pipeNetworkBaseService.getDrainageById(id);
+        if (entity == null) {
+            return AjaxResult.error("该排水管网不存在");
+        }
+        return AjaxResult.success(entity);
+    }
+}

+ 31 - 13
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/EngineeringFacilityController.java

@@ -6,20 +6,29 @@ import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
 import com.zksy.drainage.domain.EngineeringFacility;
+import com.zksy.drainage.dto.EngineeringFacilityPageInDTO;
 import com.zksy.drainage.service.EngineeringFacilityService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
 import java.util.Map;
 
 /**
- * 工程设施基础数据管理
+ * 工程设施基础数据管理(闸门 / 调蓄设施 / 泵站)
+ * <p>页面侧为只读:查询、筛选、详情与 GIS 叠加展示;
+ * 写接口保留供数据导入/后台维护,字段口径与 drainage_engineering_facility 一致。</p>
  */
 @Slf4j
 @RestController
@@ -33,13 +42,22 @@ public class EngineeringFacilityController {
     @Anonymous
     @GetMapping("/page")
     @ApiOperation(value = "工程设施分页查询")
-    public Page<EngineeringFacility> page(
-            @ApiParam("页码") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam("每页数量") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam("设施名称") @RequestParam(required = false) String name,
-            @ApiParam("行政区域") @RequestParam(required = false) String area,
-            @ApiParam("设施类型") @RequestParam(required = false) String facilityType) {
-        return service.findByPage(pageNum, pageSize, name, area, facilityType);
+    public Page<EngineeringFacility> page(EngineeringFacilityPageInDTO dto) {
+        return service.findByPage(dto);
+    }
+
+    @Anonymous
+    @GetMapping("/map")
+    @ApiOperation(value = "工程设施地图数据(不分页,仅返回带坐标的记录)")
+    public AjaxResult map(EngineeringFacilityPageInDTO dto) {
+        return AjaxResult.success(service.listForMap(dto));
+    }
+
+    @Anonymous
+    @GetMapping("/filterOptions")
+    @ApiOperation(value = "工程设施筛选下拉项")
+    public AjaxResult filterOptions() {
+        return AjaxResult.success(service.getFilterOptions());
     }
 
     @Anonymous
@@ -104,13 +122,13 @@ public class EngineeringFacilityController {
     @PostMapping("/bindEquipment")
     @ApiOperation(value = "绑定设备到工程设施")
     public AjaxResult bindEquipment(@RequestBody Map<String, Object> params) {
-        String partitionId = (String) params.get("partitionId");
+        String facilityId = (String) params.get("partitionId");
         @SuppressWarnings("unchecked")
         List<String> equipmentIds = (List<String>) params.get("equipmentIds");
-        if (partitionId == null || equipmentIds == null || equipmentIds.isEmpty()) {
+        if (facilityId == null || equipmentIds == null || equipmentIds.isEmpty()) {
             return AjaxResult.error("参数不完整");
         }
-        boolean success = service.bindEquipment(partitionId, equipmentIds);
+        boolean success = service.bindEquipment(facilityId, equipmentIds);
         return success ? AjaxResult.success() : AjaxResult.error("绑定失败");
     }
 

+ 13 - 110
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/PipeNetworkInfoController.java

@@ -2,9 +2,6 @@ package com.zksy.web.controller.drainage;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zksy.common.annotation.Anonymous;
-import com.zksy.common.annotation.Log;
-import com.zksy.common.core.domain.AjaxResult;
-import com.zksy.common.enums.BusinessType;
 import com.zksy.drainage.domain.PipeNetworkInfo;
 import com.zksy.drainage.service.PipeNetworkInfoService;
 import io.swagger.annotations.Api;
@@ -12,19 +9,25 @@ import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.List;
-import java.util.Map;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
 /**
- * 排水管网基础数据管理
+ * 排水管网基础数据查询(旧表 psgw_pipe_network_info)
+ * <p>
+ * 仅保留分页查询:综合风险区域分析、综合风险统计分析、管网运行情况三个页面
+ * 仍依赖本表的 equipment_id(关联设备)与 points(多节点坐标),
+ * 这两项在 pipe_network_base 中不存在,故暂不能迁到新接口。
+ * <p>
+ * 新增 / 修改 / 删除 / 批量删除 / 列表 / 详情 / 设备绑定等端点已无调用方,已于本次清理移除。
+ * 新页面请改用 {@link DrainagePipeNetworkController}(/api/drainage/pipeNetworkBase,数据源 pipe_network_base)。
  */
 @Slf4j
 @RestController
 @RequestMapping("/api/drainage/pipeNetwork")
-@Api(tags = "排水管网基础数据管理")
+@Api(tags = "排水管网基础数据查询(旧表,仅保留分页)")
 public class PipeNetworkInfoController {
 
     @Autowired
@@ -41,104 +44,4 @@ public class PipeNetworkInfoController {
             @ApiParam("管网类型") @RequestParam(required = false) String networkType) {
         return service.findByPage(pageNum, pageSize, name, area, networkType);
     }
-
-    /**
-     * 查询所有已被关联的设备ID列表
-     * 返回 Map: { equipmentId -> 关联的管网名称 }
-     * 可选excludeId参数排除某个管网自己的关联
-     */
-    @Anonymous
-    @GetMapping("/boundEquipmentIds")
-    @ApiOperation(value = "查询所有已关联的设备ID")
-    public AjaxResult boundEquipmentIds(
-            @ApiParam("排除的管网ID") @RequestParam(required = false) String excludeId) {
-        List<PipeNetworkInfo> all = service.list();
-        Map<String, String> boundMap = new java.util.HashMap<>();
-        for (PipeNetworkInfo p : all) {
-            if (p.getEquipmentId() != null && !p.getEquipmentId().isEmpty()) {
-                // 排除当前管网自己的关联
-                if (excludeId != null && p.getId().equals(excludeId)) continue;
-                String[] ids = p.getEquipmentId().split(",");
-                for (String id : ids) {
-                    String trimId = id.trim();
-                    if (!trimId.isEmpty()) {
-                        boundMap.put(trimId, p.getName() != null ? p.getName() : "");
-                    }
-                }
-            }
-        }
-        return AjaxResult.success(boundMap);
-    }
-
-    @Anonymous
-    @GetMapping("/{id}")
-    @ApiOperation(value = "查询排水管网详情")
-    public AjaxResult getById(@PathVariable String id) {
-        PipeNetworkInfo entity = service.getById(id);
-        if (entity == null) {
-            return AjaxResult.error("该排水管网不存在");
-        }
-        return AjaxResult.success(entity);
-    }
-
-    @Anonymous
-    @GetMapping("/list")
-    @ApiOperation(value = "查询所有排水管网")
-    public AjaxResult list() {
-        return AjaxResult.success(service.list());
-    }
-
-    @Anonymous
-    @PostMapping
-    @ApiOperation(value = "新增排水管网")
-    @Log(title = "新增排水管网", businessType = BusinessType.INSERT)
-    @Transactional
-    public AjaxResult save(@RequestBody PipeNetworkInfo entity) {
-        boolean saved = service.saveEntity(entity);
-        return saved ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
-    }
-
-    @Anonymous
-    @PutMapping
-    @ApiOperation(value = "修改排水管网")
-    @Log(title = "修改排水管网", businessType = BusinessType.UPDATE)
-    @Transactional
-    public AjaxResult update(@RequestBody PipeNetworkInfo entity) {
-        boolean updated = service.updateEntity(entity);
-        return updated ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/{id}")
-    @ApiOperation(value = "删除排水管网")
-    @Log(title = "删除排水管网", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult deleteById(@PathVariable String id) {
-        boolean deleted = service.removeEntity(id);
-        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/batch")
-    @ApiOperation(value = "批量删除排水管网")
-    @Log(title = "批量删除排水管网", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult deleteBatch(@RequestBody List<String> ids) {
-        boolean deleted = service.removeBatchEntity(ids);
-        return deleted ? AjaxResult.success("批量删除成功") : AjaxResult.error("批量删除失败");
-    }
-
-    @Anonymous
-    @PostMapping("/bindEquipment")
-    @ApiOperation(value = "绑定设备到排水管网")
-    public AjaxResult bindEquipment(@RequestBody Map<String, Object> params) {
-        String partitionId = (String) params.get("partitionId");
-        @SuppressWarnings("unchecked")
-        List<String> equipmentIds = (List<String>) params.get("equipmentIds");
-        if (partitionId == null || equipmentIds == null || equipmentIds.isEmpty()) {
-            return AjaxResult.error("参数不完整");
-        }
-        boolean success = service.bindEquipment(partitionId, equipmentIds);
-        return success ? AjaxResult.success() : AjaxResult.error("绑定失败");
-    }
 }

+ 38 - 18
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/WaterloggingPointController.java

@@ -6,20 +6,29 @@ import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
 import com.zksy.drainage.domain.WaterloggingPoint;
+import com.zksy.drainage.dto.WaterloggingPointPageInDTO;
 import com.zksy.drainage.service.WaterloggingPointService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
 /**
  * 易积水点管理
+ * <p>易积水点关联的是视频摄像头,不是通用监测设备;前端在地图/列表中点击可直接播放关联摄像头。</p>
  */
 @Slf4j
 @RestController
@@ -33,12 +42,22 @@ public class WaterloggingPointController {
     @Anonymous
     @GetMapping("/page")
     @ApiOperation(value = "易积水点分页查询")
-    public Page<WaterloggingPoint> page(
-            @ApiParam("页码") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam("每页数量") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam("监测点名称") @RequestParam(required = false) String name,
-            @ApiParam("行政区域") @RequestParam(required = false) String area) {
-        return service.findByPage(pageNum, pageSize, name, area);
+    public Page<WaterloggingPoint> page(WaterloggingPointPageInDTO dto) {
+        return service.findByPage(dto);
+    }
+
+    @Anonymous
+    @GetMapping("/map")
+    @ApiOperation(value = "易积水点地图数据(不分页,仅返回带坐标的记录)")
+    public AjaxResult map(WaterloggingPointPageInDTO dto) {
+        return AjaxResult.success(service.listForMap(dto));
+    }
+
+    @Anonymous
+    @GetMapping("/filterOptions")
+    @ApiOperation(value = "易积水点筛选下拉项")
+    public AjaxResult filterOptions() {
+        return AjaxResult.success(service.getFilterOptions());
     }
 
     @Anonymous
@@ -100,16 +119,17 @@ public class WaterloggingPointController {
     }
 
     @Anonymous
-    @PostMapping("/bindEquipment")
-    @ApiOperation(value = "绑定设备到易积水点")
-    public AjaxResult bindEquipment(@RequestBody Map<String, Object> params) {
-        String partitionId = (String) params.get("partitionId");
-        @SuppressWarnings("unchecked")
-        List<String> equipmentIds = (List<String>) params.get("equipmentIds");
-        if (partitionId == null || equipmentIds == null || equipmentIds.isEmpty()) {
-            return AjaxResult.error("参数不完整");
+    @PostMapping("/{id}/cameras")
+    @ApiOperation(value = "关联摄像头到易积水点(整体替换,空列表表示解除全部)")
+    public AjaxResult bindCameras(@PathVariable String id, @RequestBody Map<String, Object> params) {
+        Object raw = params == null ? null : params.get("cameraIds");
+        List<String> cameraIds = new ArrayList<>();
+        if (raw instanceof List) {
+            for (Object item : (List<?>) raw) {
+                if (item != null) cameraIds.add(String.valueOf(item));
+            }
         }
-        boolean success = service.bindEquipment(partitionId, equipmentIds);
-        return success ? AjaxResult.success() : AjaxResult.error("绑定失败");
+        boolean success = service.bindCameras(id, cameraIds);
+        return success ? AjaxResult.success("关联成功") : AjaxResult.error("关联失败");
     }
 }

+ 160 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/mobile/MobileEmergencyController.java

@@ -0,0 +1,160 @@
+package com.zksy.web.controller.mobile;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.dispatch.dto.*;
+import com.zksy.dispatch.enums.EventStatusEnum;
+import com.zksy.dispatch.enums.PlanStatusEnum;
+import com.zksy.dispatch.service.DisEventService;
+import com.zksy.dispatch.service.DisPlanService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/mobile/emergency")
+@Api(tags = "移动端应急处置")
+@RequiredArgsConstructor
+@PreAuthorize("isAuthenticated()")
+public class MobileEmergencyController {
+
+    private final DisEventService eventService;
+    private final DisPlanService planService;
+
+    @GetMapping("/home")
+    @ApiOperation("移动端应急主页统计和最新动态")
+    public AjaxResult home() {
+        DisHomeVO home = eventService.listHome();
+        DisEventPageDTO query = new DisEventPageDTO();
+        query.setStatus(EventStatusEnum.DISPOSING.getCode());
+        query.setPageNum(1L);
+        query.setPageSize(5L);
+        Page<DisEventVO> latest = eventService.findByPage(query);
+        return AjaxResult.success(new java.util.LinkedHashMap<String, Object>() {{
+            put("pendingCount", home.getPendingCount());
+            put("disposingCount", home.getDisposingCount());
+            put("releasedCount", eventService.findByPage(monthlyReleasedQuery()).getTotal());
+            put("dynamics", latest.getRecords());
+        }});
+    }
+
+    private DisEventPageDTO monthlyReleasedQuery() {
+        DisEventPageDTO query = new DisEventPageDTO();
+        query.setStatus(EventStatusEnum.RELEASED.getCode());
+        query.setStartDate(java.time.LocalDate.now().withDayOfMonth(1).toString());
+        query.setPageNum(1L);
+        query.setPageSize(1L);
+        return query;
+    }
+
+    @GetMapping("/events")
+    @ApiOperation("移动端事件信息分页")
+    public AjaxResult events(DisEventPageDTO query) {
+        return AjaxResult.success(eventService.findByPage(query));
+    }
+
+    @GetMapping("/events/{id}")
+    @ApiOperation("移动端事件详情")
+    public AjaxResult eventDetail(@PathVariable Long id) {
+        return AjaxResult.success(eventService.getDetail(id));
+    }
+
+    @PostMapping("/events/{id}/verify")
+    @ApiOperation("移动端核实事件")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:verify')")
+    public AjaxResult verify(@PathVariable Long id, @RequestBody EventVerifyDTO dto) {
+        dto.setId(id);
+        eventService.verify(id, dto);
+        return AjaxResult.success("核实成功");
+    }
+
+    @PostMapping("/events/{id}/start")
+    @ApiOperation("移动端开始处置")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:handle')")
+    public AjaxResult start(@PathVariable Long id, @RequestBody EventTrackDTO dto) {
+        dto.setEventId(id);
+        eventService.startDispose(dto);
+        return AjaxResult.success("已开始处置");
+    }
+
+    @PostMapping("/events/{id}/transfer")
+    @ApiOperation("移动端事件转接")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:dispatch')")
+    public AjaxResult transfer(@PathVariable Long id, @RequestBody EventTransferDTO dto) {
+        dto.setEventId(id);
+        eventService.transfer(dto);
+        return AjaxResult.success("转接成功");
+    }
+
+    @PostMapping("/events/{id}/report")
+    @ApiOperation("移动端事件报送")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:dispatch')")
+    public AjaxResult report(@PathVariable Long id, @RequestBody EventTransferDTO dto) {
+        dto.setEventId(id);
+        eventService.report(dto);
+        return AjaxResult.success("报送成功");
+    }
+
+    @PostMapping("/events/{id}/track")
+    @ApiOperation("移动端处置跟踪")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:handle')")
+    public AjaxResult track(@PathVariable Long id, @RequestBody EventTrackDTO dto) {
+        dto.setEventId(id);
+        eventService.track(dto);
+        return AjaxResult.success("跟踪记录保存成功");
+    }
+
+    @PostMapping("/events/{id}/submit-verify")
+    @ApiOperation("移动端处置人提交核验")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:handle')")
+    public AjaxResult submitVerify(@PathVariable Long id, @RequestBody EventTrackDTO dto) {
+        dto.setEventId(id);
+        eventService.submitVerify(dto);
+        return AjaxResult.success("已提交核验");
+    }
+
+    @PostMapping("/events/{id}/audit")
+    @ApiOperation("移动端最终核验")
+    @PreAuthorize("@ss.hasPermi('dispatch:event:audit')")
+    public AjaxResult audit(@PathVariable Long id, @RequestBody EventAuditDTO dto) {
+        eventService.audit(id, dto);
+        return AjaxResult.success("核验完成");
+    }
+
+    @GetMapping("/plans")
+    @ApiOperation("移动端已通过处置方案")
+    public AjaxResult plans(PlanPageDTO query) {
+        query.setStatus(PlanStatusEnum.APPROVED.getCode());
+        return AjaxResult.success(planService.findByPage(query));
+    }
+
+    @GetMapping("/plans/{id}")
+    @ApiOperation("移动端处置方案详情")
+    public AjaxResult planDetail(@PathVariable Long id) {
+        return AjaxResult.success(planService.getDetail(id));
+    }
+
+    @GetMapping("/audit")
+    @ApiOperation("移动端预案审核列表")
+    public AjaxResult audit(PlanPageDTO query) {
+        return AjaxResult.success(planService.findByPage(query));
+    }
+
+    @PostMapping("/audit/{id}")
+    @ApiOperation("移动端预案审核(通过 / 驳回)")
+    @PreAuthorize("@ss.hasPermi('dispatch:plan:approve')")
+    public AjaxResult review(@PathVariable Long id, @RequestBody PlanApproveDTO dto) {
+        Boolean approved = dto.getApproved();
+        if (null == approved) {
+            return AjaxResult.error("请选择审核结果");
+        }
+        if (approved) {
+            planService.approve(id, dto);
+            return AjaxResult.success("审批通过");
+        }
+        planService.reject(id, dto);
+        return AjaxResult.success("已驳回");
+    }
+}

+ 17 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/system/SysLoginController.java

@@ -26,6 +26,7 @@ import com.zksy.framework.web.service.SysPermissionService;
 import com.zksy.framework.web.service.TokenService;
 import com.zksy.system.service.ISysConfigService;
 import com.zksy.system.service.ISysMenuService;
+import com.zksy.system.service.ISysUserService;
 
 /**
  * 登录验证
@@ -41,6 +42,9 @@ public class SysLoginController
     @Autowired
     private ISysMenuService menuService;
 
+    @Autowired
+    private ISysUserService userService;
+
     @Autowired
     private SysPermissionService permissionService;
 
@@ -100,7 +104,19 @@ public class SysLoginController
     public AjaxResult getInfo()
     {
         LoginUser loginUser = SecurityUtils.getLoginUser();
-        SysUser user = loginUser.getUser();
+        // Reload the user and its role relations on every request.  Role/menu
+        // assignments are mutable data and must not remain frozen in the
+        // LoginUser object that was created at sign-in time.
+        SysUser user = userService.selectUserById(loginUser.getUserId());
+        if (user == null)
+        {
+            user = loginUser.getUser();
+        }
+        else
+        {
+            loginUser.setUser(user);
+            loginUser.setDeptId(user.getDeptId());
+        }
         // 角色集合
         Set<String> roles = permissionService.getRolePermission(user);
         // 权限集合

+ 4 - 4
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureBaseArchivesAdapterTest.java

@@ -70,10 +70,10 @@ class BusinessCockpitInfrastructureBaseArchivesAdapterTest {
         pipe.setBuildDate(java.sql.Date.valueOf(LocalDate.of(2010, 3, 4)));
 
         EngineeringFacility drainage = new EngineeringFacility();
-        drainage.setId("DRAIN-001");
-        drainage.setName("一号调蓄池");
-        drainage.setArea("CB");
-        drainage.setFacilityType("调蓄设施");
+        drainage.setFacilityId("DRAIN-001");
+        drainage.setFacilityName("一号调蓄池");
+        drainage.setLocation("CB");
+        drainage.setFacilityType("1");
 
         when(plantService.list()).thenReturn(Collections.singletonList(plant));
         when(pumpStationService.list()).thenReturn(Collections.singletonList(pump));

+ 4 - 5
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureOperatingInformationAdapterTest.java

@@ -73,10 +73,9 @@ class BusinessCockpitInfrastructureOperatingInformationAdapterTest {
         pipe.setUpdateTime(LocalDateTime.parse("2026-09-02T09:57:00"));
 
         EngineeringFacility facility = new EngineeringFacility();
-        facility.setId("DRAIN-001");
-        facility.setName("一号调蓄池");
-        facility.setFacilityType("调蓄设施");
-        facility.setPointCount(3);
+        facility.setFacilityId("DRAIN-001");
+        facility.setFacilityName("一号调蓄池");
+        facility.setFacilityType("1");
         facility.setUpdateTime(LocalDateTime.parse("2026-09-02T09:56:00"));
 
         when(plantService.list()).thenReturn(Collections.singletonList(plant));
@@ -113,7 +112,7 @@ class BusinessCockpitInfrastructureOperatingInformationAdapterTest {
                         .findFirst()
                         .orElseThrow(AssertionError::new);
         assertEquals("DRAINAGE", drainageMetric.getIndustry());
-        assertEquals(new BigDecimal("3"), drainageMetric.getValue());
+        assertEquals(BigDecimal.ONE, drainageMetric.getValue());
     }
 
     @Test

+ 5 - 5
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitFacilityCountCrossEndpointContractTest.java

@@ -56,13 +56,13 @@ class CockpitFacilityCountCrossEndpointContractTest {
         ManholeBase manhole = new ManholeBase();
         manhole.setManholeCode("MH-1");
         EngineeringFacility gate = new EngineeringFacility();
-        gate.setId("GATE-1");
-        gate.setFacilityType("闸门");
+        gate.setFacilityId("GATE-1");
+        gate.setFacilityType("0");
         EngineeringFacility pump = new EngineeringFacility();
-        pump.setId("ENGINEERING-PUMP");
-        pump.setFacilityType("泵站");
+        pump.setFacilityId("ENGINEERING-PUMP");
+        pump.setFacilityType("2");
         EngineeringFacility unknown = new EngineeringFacility();
-        unknown.setId("UNKNOWN-1");
+        unknown.setFacilityId("UNKNOWN-1");
         unknown.setFacilityType("unclassified");
         WaterPlantInfo missingCode = new WaterPlantInfo();
         when(plants.list()).thenReturn(Arrays.asList(plant, missingCode));

+ 5 - 23
pipe-network-service/zksy-framework/src/main/java/com/zksy/framework/web/service/SysPermissionService.java

@@ -1,15 +1,10 @@
 package com.zksy.framework.web.service;
 
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
-import org.springframework.util.CollectionUtils;
-import com.zksy.common.constant.UserConstants;
-import com.zksy.common.core.domain.entity.SysRole;
 import com.zksy.common.core.domain.entity.SysUser;
-import com.zksy.common.utils.StringUtils;
 import com.zksy.system.service.ISysMenuService;
 import com.zksy.system.service.ISysRoleService;
 
@@ -64,24 +59,11 @@ public class SysPermissionService
         }
         else
         {
-            List<SysRole> roles = user.getRoles();
-            if (!CollectionUtils.isEmpty(roles))
-            {
-                // 多角色设置permissions属性,以便数据权限匹配权限
-                for (SysRole role : roles)
-                {
-                    if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin())
-                    {
-                        Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
-                        role.setPermissions(rolePerms);
-                        perms.addAll(rolePerms);
-                    }
-                }
-            }
-            else
-            {
-                perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
-            }
+            // Always resolve from the current user-role/menu relations.  The
+            // SysUser object stored in LoginUser may contain the roles loaded
+            // at login time, so relying on user.getRoles() can return stale
+            // permissions after an administrator changes a user's roles.
+            perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
         }
         return perms;
     }

+ 1 - 1
pipe-network-service/zksy-framework/src/main/java/com/zksy/framework/web/service/UserDetailsServiceImpl.java

@@ -37,7 +37,7 @@ public class UserDetailsServiceImpl implements UserDetailsService
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
     {
-        SysUser user = userService.selectUserByUserName(username);
+        SysUser user = userService.selectUserByUserNameOrPhone(username);
         if (StringUtils.isNull(user))
         {
             log.info("登录用户:{} 不存在.", username);

+ 11 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/PipeNetworkBase.java

@@ -29,6 +29,17 @@ public class PipeNetworkBase {
      */
     public static final String NETWORK_TYPE_GAS = "0";
 
+    /**
+     * 设施类型:排水设施(network_type = 1)
+     * <p>排水模块所有查询 pipe_network_base 的接口都必须带上该条件</p>
+     */
+    public static final String NETWORK_TYPE_DRAINAGE = "1";
+
+    /**
+     * 设施类型:供水设施(network_type = 2)
+     */
+    public static final String NETWORK_TYPE_WATER = "2";
+
     @TableId(value = "network_id", type = IdType.ASSIGN_UUID)
     @ApiModelProperty("管网设施ID")
     private String networkId;

+ 20 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/dto/PipeNetworkPageInDTO.java

@@ -5,12 +5,16 @@ import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 @Data
 @ApiModel(value = "管网分页查询入参", description = "管网分页查询入参")
 public class PipeNetworkPageInDTO implements Serializable {
     private static final long serialVersionUID = 1L;
 
+    @ApiModelProperty(value = "设施编码")
+    private String networkCode;
+
     @ApiModelProperty(value = "管网名称")
     private String networkName;
 
@@ -26,7 +30,22 @@ public class PipeNetworkPageInDTO implements Serializable {
     @ApiModelProperty(value = "状态")
     private String status;
 
-    @ApiModelProperty(value = "当前记录起始索引", required = true)
+    @ApiModelProperty(value = "位置描述(模糊匹配)")
+    private String location;
+
+    @ApiModelProperty(value = "建设日期起(yyyy-MM-dd,含)")
+    private String buildDateStart;
+
+    @ApiModelProperty(value = "建设日期止(yyyy-MM-dd,含)")
+    private String buildDateEnd;
+
+    @ApiModelProperty(value = "管径下限(毫米,含)")
+    private BigDecimal diameterMin;
+
+    @ApiModelProperty(value = "管径上限(毫米,含)")
+    private BigDecimal diameterMax;
+
+    @ApiModelProperty(value = "当前页码", required = true)
     private Long pageNum;
 
     @ApiModelProperty(value = "每页显示记录数", required = true)

+ 25 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/PipeNetworkBaseService.java

@@ -7,6 +7,7 @@ import com.zksy.base.dto.PipeNetworkExportDTO;
 import com.zksy.base.dto.PipeNetworkPageInDTO;
 
 import java.util.List;
+import java.util.Map;
 
 public interface PipeNetworkBaseService extends IService<PipeNetworkBase> {
 
@@ -41,6 +42,30 @@ public interface PipeNetworkBaseService extends IService<PipeNetworkBase> {
      */
     List<PipeNetworkExportDTO> exportGasData(String networkName, String status);
 
+    /* ==================== 排水模块专用查询(强制 network_type=1) ==================== */
+
+    /**
+     * 排水设施分页查询(支持编码/名称/等级/管养单位/材质/状态/位置/建设日期区间/管径区间)
+     * <p>仅查询 network_type=1 的排水设施</p>
+     */
+    Page<PipeNetworkBase> queryDrainageByPage(PipeNetworkPageInDTO dto);
+
+    /**
+     * 按ID查询排水设施
+     * <p>仅查询 network_type=1 的排水设施,非排水设施返回 null</p>
+     */
+    PipeNetworkBase getDrainageById(String networkId);
+
+    /**
+     * 排水设施地图数据(不分页,仅返回起终点至少一端有坐标的记录)
+     * <p>筛选条件与分页查询一致,用于按筛选结果绘制管网</p>
+     */
+    List<PipeNetworkBase> listDrainageForMap(PipeNetworkPageInDTO dto);
+
+    /**
+     * 排水设施筛选下拉项(材质、管养单位去重值)
+     */
+    Map<String, List<String>> getDrainageFilterOptions();
     boolean saveWithCheck(PipeNetworkBase entity);
 
     boolean saveBatchWithCheck(List<PipeNetworkBase> entityList);

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

@@ -1,6 +1,7 @@
 package com.zksy.base.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -15,8 +16,12 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -215,6 +220,113 @@ public class PipeNetworkBaseServiceImpl extends ServiceImpl<PipeNetworkBaseMappe
         return buildExportList(this.list(wrapper));
     }
 
+    /* ==================== 排水模块专用查询(强制 network_type=1) ==================== */
+
+    @Override
+    public Page<PipeNetworkBase> queryDrainageByPage(PipeNetworkPageInDTO dto) {
+        long pageNum = dto.getPageNum() == null ? 1L : dto.getPageNum();
+        long pageSize = dto.getPageSize() == null ? 10L : dto.getPageSize();
+        return this.page(new Page<>(pageNum, pageSize), buildDrainageWrapper(dto));
+    }
+
+    @Override
+    public PipeNetworkBase getDrainageById(String networkId) {
+        if (networkId == null || networkId.isEmpty()) {
+            return null;
+        }
+        LambdaQueryWrapper<PipeNetworkBase> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PipeNetworkBase::getNetworkId, networkId);
+        // 排水模块只允许查询排水设施
+        wrapper.eq(PipeNetworkBase::getNetworkType, PipeNetworkBase.NETWORK_TYPE_DRAINAGE);
+        return this.getOne(wrapper, false);
+    }
+
+    @Override
+    public List<PipeNetworkBase> listDrainageForMap(PipeNetworkPageInDTO dto) {
+        LambdaQueryWrapper<PipeNetworkBase> wrapper = buildDrainageWrapper(dto);
+        // 起终点至少一端有坐标才返回,避免前端拿到无法绘制的空几何
+        wrapper.and(w -> w
+                .and(s -> s.isNotNull(PipeNetworkBase::getStartLongitude)
+                        .isNotNull(PipeNetworkBase::getStartLatitude))
+                .or()
+                .and(e -> e.isNotNull(PipeNetworkBase::getEndLongitude)
+                        .isNotNull(PipeNetworkBase::getEndLatitude)));
+        return this.list(wrapper);
+    }
+
+    /**
+     * 排水设施筛选下拉项(材质、管养单位去重值)
+     */
+    @Override
+    public Map<String, List<String>> getDrainageFilterOptions() {
+        Map<String, List<String>> options = new HashMap<>();
+        options.put("materials", distinctDrainageColumn("material"));
+        options.put("managementUnits", distinctDrainageColumn("management_unit"));
+        return options;
+    }
+
+    /**
+     * 取排水设施某列的去重非空值
+     * <p>列名仅由内部常量传入,不接受外部输入</p>
+     */
+    private List<String> distinctDrainageColumn(String column) {
+        QueryWrapper<PipeNetworkBase> wrapper = new QueryWrapper<>();
+        wrapper.select("DISTINCT " + column)
+                .eq("network_type", PipeNetworkBase.NETWORK_TYPE_DRAINAGE)
+                .isNotNull(column)
+                .ne(column, "")
+                .orderByAsc(column);
+        return this.listObjs(wrapper, obj -> obj == null ? null : obj.toString())
+                .stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 排水设施查询条件(强制 network_type=1,分页与地图共用)
+     */
+    private LambdaQueryWrapper<PipeNetworkBase> buildDrainageWrapper(PipeNetworkPageInDTO dto) {
+        LambdaQueryWrapper<PipeNetworkBase> wrapper = new LambdaQueryWrapper<>();
+        // 排水模块只允许查询排水设施
+        wrapper.eq(PipeNetworkBase::getNetworkType, PipeNetworkBase.NETWORK_TYPE_DRAINAGE);
+        wrapper.like(hasText(dto.getNetworkCode()), PipeNetworkBase::getNetworkCode, dto.getNetworkCode());
+        wrapper.like(hasText(dto.getNetworkName()), PipeNetworkBase::getNetworkName, dto.getNetworkName());
+        wrapper.eq(hasText(dto.getNetworkLevel()), PipeNetworkBase::getNetworkLevel, dto.getNetworkLevel());
+        wrapper.like(hasText(dto.getManagementUnit()), PipeNetworkBase::getManagementUnit, dto.getManagementUnit());
+        wrapper.like(hasText(dto.getMaterial()), PipeNetworkBase::getMaterial, dto.getMaterial());
+        wrapper.eq(hasText(dto.getStatus()), PipeNetworkBase::getStatus, dto.getStatus());
+        wrapper.like(hasText(dto.getLocation()), PipeNetworkBase::getLocation, dto.getLocation());
+
+        Date buildStart = parseDate(dto.getBuildDateStart());
+        Date buildEnd = parseDate(dto.getBuildDateEnd());
+        wrapper.ge(buildStart != null, PipeNetworkBase::getBuildDate, buildStart);
+        wrapper.le(buildEnd != null, PipeNetworkBase::getBuildDate, buildEnd);
+        wrapper.ge(dto.getDiameterMin() != null, PipeNetworkBase::getDiameter, dto.getDiameterMin());
+        wrapper.le(dto.getDiameterMax() != null, PipeNetworkBase::getDiameter, dto.getDiameterMax());
+
+        wrapper.orderByDesc(PipeNetworkBase::getCreateTime);
+        return wrapper;
+    }
+
+    private static boolean hasText(String value) {
+        return value != null && !value.trim().isEmpty();
+    }
+
+    /**
+     * 解析 yyyy-MM-dd,格式非法时返回 null(视为不按该端筛选)
+     */
+    private static Date parseDate(String value) {
+        if (!hasText(value)) {
+            return null;
+        }
+        try {
+            return java.sql.Date.valueOf(value.trim().substring(0, 10));
+        } catch (RuntimeException e) {
+            log.warn("排水管网查询忽略非法日期参数:{}", value);
+            return null;
+        }
+    }
+
     /**
      * 管网设施列表转导出DTO
      */

+ 6 - 5
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureBaseArchivesAdapter.java

@@ -103,8 +103,8 @@ public class BusinessCockpitInfrastructureBaseArchivesAdapter
             updatedAt = latest(updatedAt, facility.getUpdateTime());
             updatedAt = latest(updatedAt, facility.getCreateTime());
             // 保留现有工程设施年限映射;本票只统一设施范围,不裁定建设日期来源。
-            add(facilities, key("psgw_engineering_facility", facility.getId(), facility.getId()),
-                    facility.getId(), drainageType(facility.getFacilityType()), null, null,
+            add(facilities, key("drainage_engineering_facility", facility.getFacilityId(), facility.getFacilityCode()),
+                    facility.getFacilityId(), drainageType(facility.getFacilityType()), null, null,
                     facility.getCreateTime() == null ? null : facility.getCreateTime().toLocalDate());
         }
         for (WaterSourceInfo source : safeList(sources.list())) {
@@ -142,9 +142,10 @@ public class BusinessCockpitInfrastructureBaseArchivesAdapter
     private static String drainageType(String value) {
         String type = trimToNull(value);
         if (type == null) return UNCLASSIFIED;
-        if (type.contains("闸")) return "DRAINAGE_GATE";
-        if (type.contains("调蓄") || type.contains("蓄水")) return "DRAINAGE_STORAGE";
-        if (type.contains("泵站")) return "DRAINAGE_PUMP_STATION";
+        // 新口径为字典编码(drainage_facility_type:0闸门 1调蓄设施 2泵站),中文值仅作存量兼容
+        if (EngineeringFacility.TYPE_GATE.equals(type) || type.contains("闸")) return "DRAINAGE_GATE";
+        if (EngineeringFacility.TYPE_STORAGE.equals(type) || type.contains("调蓄") || type.contains("蓄水")) return "DRAINAGE_STORAGE";
+        if (EngineeringFacility.TYPE_PUMP.equals(type) || type.contains("泵站")) return "DRAINAGE_PUMP_STATION";
         return UNCLASSIFIED;
     }
 

+ 9 - 7
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureOperatingInformationAdapter.java

@@ -128,7 +128,7 @@ public class BusinessCockpitInfrastructureOperatingInformationAdapter
         }
 
         for (EngineeringFacility facility : safeList(engineeringFacilityService.list())) {
-            if (facility == null || isBlank(facility.getId()) || facility.getPointCount() == null) {
+            if (facility == null || isBlank(facility.getFacilityId())) {
                 continue;
             }
             String typeCode = drainageFacilityType(facility.getFacilityType());
@@ -137,17 +137,18 @@ public class BusinessCockpitInfrastructureOperatingInformationAdapter
             }
             String sampledAt = format(facility.getUpdateTime());
             latestUpdatedAt = latest(latestUpdatedAt, sampledAt);
+            // 工程设施是点要素,每个设施计 1 个点(旧表 point_count 已随表结构重建移除)
             metrics.add(metric(
                     "drainage_" + typeCode.toLowerCase() + "_point_count",
-                    isBlank(facility.getName()) ? "排水设施点数" : facility.getName() + "点数",
+                    isBlank(facility.getFacilityName()) ? "排水设施点数" : facility.getFacilityName() + "点数",
                     "CAPACITY",
                     "个",
                     "DRAINAGE",
-                    BigDecimal.valueOf(facility.getPointCount()),
+                    BigDecimal.ONE,
                     null,
                     sampledAt,
                     false,
-                    facility.getId()));
+                    facility.getFacilityId()));
         }
 
         return new CockpitInfrastructureOperatingInformationSnapshot(latestUpdatedAt, metrics);
@@ -193,13 +194,14 @@ public class BusinessCockpitInfrastructureOperatingInformationAdapter
         if (normalized == null) {
             return null;
         }
-        if (normalized.contains("闸")) {
+        // 新口径为字典编码(drainage_facility_type:0闸门 1调蓄设施 2泵站),中文值仅作存量兼容
+        if (EngineeringFacility.TYPE_GATE.equals(normalized) || normalized.contains("闸")) {
             return "gate";
         }
-        if (normalized.contains("调蓄") || normalized.contains("蓄水")) {
+        if (EngineeringFacility.TYPE_STORAGE.equals(normalized) || normalized.contains("调蓄") || normalized.contains("蓄水")) {
             return "storage";
         }
-        if (normalized.contains("泵站")) {
+        if (EngineeringFacility.TYPE_PUMP.equals(normalized) || normalized.contains("泵站")) {
             return "pump_station";
         }
         return null;

+ 3 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEvent.java

@@ -18,8 +18,8 @@ import java.time.LocalDateTime;
  * 应急事件主表(dis_event)
  * 一个事件一行,承载全生命周期「当前值」;过程留痕一律写 DisEventLog。
  * 状态机:
- * 0 待核实 → 1 已核实 → 2 处置中 → 3 已解除;
- * 旁路 4 不实事件(初判核实判定为不实,直接终止)。终态为 3、4。
+ * 0 待核实 → 1 已核实 → 2 处置中 → 3 待验收 → 4 已解除;
+ * 旁路 5 不实事件(初判核实判定为不实,直接终止)。终态为 4、5。
  * 转接、报送、处置跟踪、工作催办均不改变状态。
  * 字段统一加 event_ 前缀,规避 KingbaseES 下 level 为保留字需转义的问题。
  * @author zengy
@@ -68,7 +68,7 @@ public class DisEvent {
     @ApiModelProperty("事件等级:字典 dis_event_level")
     private String eventLevel;
 
-    @ApiModelProperty("事件状态:EventStatusEnum —— 0待核实 1已核实 2处置中 3已解除 4不实事件")
+    @ApiModelProperty("事件状态:EventStatusEnum —— 0待核实 1已核实 2处置中 3待验收 4已解除 5不实事件")
     private Integer status;
 
     @ApiModelProperty("核实结论:0-真实 1-不实;仅状态为已核实/不实事件时有值")
@@ -96,9 +96,6 @@ public class DisEvent {
     @ApiModelProperty("责任处置人电话")
     private String handlerPhone;
 
-    @ApiModelProperty("责任单位 / 转接接收部门")
-    private String department;
-
     @ApiModelProperty("开始处置时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime disposeTime;

+ 9 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEventLog.java

@@ -36,7 +36,7 @@ public class DisEventLog {
     @ApiModelProperty("事件编号(冗余,便于按编号单独查日志)")
     private String eventNo;
 
-    @ApiModelProperty("操作类型:EventLogOperTypeEnum —— 0接报登记 1初判核实 2不实核销 3电话转接 4上报报送 5开始处置 6处置跟踪 7事件解除 8工作催办 9信息修改")
+    @ApiModelProperty("操作类型:EventLogOperTypeEnum —— 0接报登记 1初判核实 2不实核销 3电话转接 4上报报送 5开始处置 6处置跟踪 7事件解除 8工作催办 9信息修改 10提交核验 11最终核验")
     private Integer operType;
 
     @ApiModelProperty("操作说明/备注:跟踪内容、催办内容、核实意见、解除说明等")
@@ -66,9 +66,17 @@ public class DisEventLog {
     @ApiModelProperty("态势评估:字典 dis_event_trend(oper_type=6)")
     private String trend;
 
+    @TableField(exist = false)
+    @ApiModelProperty("态势评估名称:字典 dis_event_trend")
+    private String trendName;
+
     @ApiModelProperty("是否需要升级:字典 dis_yes_no(oper_type=6)")
     private Integer needUpgrade;
 
+    @TableField(exist = false)
+    @ApiModelProperty("是否需要升级名称:字典 dis_yes_no")
+    private String needUpgradeName;
+
     @ApiModelProperty("下次回访时间(oper_type=6)")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime nextVisitTime;

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisPlan.java

@@ -41,9 +41,17 @@ public class DisPlan {
     @ApiModelProperty("预案类型:字典 dis_plan_type")
     private String planType;
 
+    @TableField(exist = false)
+    @ApiModelProperty("预案类型名称:字典 dis_plan_type")
+    private String planTypeName;
+
     @ApiModelProperty("预案等级:字典 dis_plan_level")
     private String planLevel;
 
+    @TableField(exist = false)
+    @ApiModelProperty("预案等级名称:字典 dis_plan_level")
+    private String planLevelName;
+
     @ApiModelProperty("编制单位")
     private String compileUnit;
 

+ 3 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisDeptOptionVO.java

@@ -8,14 +8,11 @@ import java.io.Serializable;
 
 /**
  * 部门下拉项(系统部门)
- * 初判核实时「处置单位」选择系统部门(sys_dept);前端用 deptId 树形展示并
- * 过滤处置人候选(所选部门及其所有下级部门),提交时把
- * deptName 写入 dis_event.department(该列存部门中文名,工作催办、
- * 多条件筛选均按名称口径,保持不变)。
+ * 用于系统部门选择器,事件核实不再依赖部门选择。
  * @author zengy
  */
 @Data
-@ApiModel(value = "部门下拉项", description = "应急事件处置单位可选系统部门")
+@ApiModel(value = "部门下拉项", description = "可选系统部门")
 public class DisDeptOptionVO implements Serializable {
 
     private static final long serialVersionUID = 1L;
@@ -26,7 +23,7 @@ public class DisDeptOptionVO implements Serializable {
     @ApiModelProperty("父部门ID;前端据此建树")
     private Long parentId;
 
-    @ApiModelProperty("部门名称;提交时写入 dis_event.department")
+    @ApiModelProperty("部门名称;用于选择器展示")
     private String deptName;
 
     @ApiModelProperty("显示顺序;同级排序用")

+ 3 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventPageDTO.java

@@ -21,6 +21,9 @@ public class DisEventPageDTO implements Serializable {
     @ApiModelProperty(value = "事件编号(模糊)")
     private String eventNo;
 
+    @ApiModelProperty(value = "事件编号或事件描述统一关键字(模糊)")
+    private String keyword;
+
     @ApiModelProperty(value = "事件描述(模糊)")
     private String description;
 
@@ -36,9 +39,6 @@ public class DisEventPageDTO implements Serializable {
     @ApiModelProperty(value = "发生地点(模糊)")
     private String location;
 
-    @ApiModelProperty(value = "责任单位(模糊)")
-    private String department;
-
     @ApiModelProperty(value = "责任处置人(模糊)")
     private String handler;
 

+ 18 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventVO.java

@@ -34,9 +34,15 @@ public class DisEventVO implements Serializable {
     @ApiModelProperty(value = "事件来源:字典 dis_event_source")
     private String eventSource;
 
+    @ApiModelProperty(value = "事件来源名称:字典 dis_event_source")
+    private String eventSourceName;
+
     @ApiModelProperty(value = "来源渠道:字典 dis_event_channel")
     private String channel;
 
+    @ApiModelProperty(value = "来源渠道名称:字典 dis_event_channel")
+    private String channelName;
+
     @ApiModelProperty(value = "发生地点")
     private String location;
 
@@ -63,9 +69,15 @@ public class DisEventVO implements Serializable {
     @ApiModelProperty(value = "事件类别:字典 dis_event_category")
     private String eventCategory;
 
+    @ApiModelProperty(value = "事件类别名称:字典 dis_event_category")
+    private String eventCategoryName;
+
     @ApiModelProperty(value = "事件等级:字典 dis_event_level")
     private String eventLevel;
 
+    @ApiModelProperty(value = "事件等级名称:字典 dis_event_level")
+    private String eventLevelName;
+
     @ApiModelProperty(value = "事件状态:EventStatusEnum")
     private Integer status;
 
@@ -100,9 +112,6 @@ public class DisEventVO implements Serializable {
     @ApiModelProperty(value = "责任处置人电话")
     private String handlerPhone;
 
-    @ApiModelProperty(value = "责任单位")
-    private String department;
-
     @ApiModelProperty(value = "开始处置时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime disposeTime;
@@ -114,6 +123,9 @@ public class DisEventVO implements Serializable {
     @ApiModelProperty(value = "解除原因:字典 dis_event_release_reason")
     private String releaseReason;
 
+    @ApiModelProperty(value = "解除原因名称:字典 dis_event_release_reason")
+    private String releaseReasonName;
+
     @ApiModelProperty(value = "解除说明")
     private String releaseNote;
 
@@ -128,6 +140,9 @@ public class DisEventVO implements Serializable {
     @ApiModelProperty(value = "是否需要升级:字典 dis_yes_no")
     private Integer needUpgrade;
 
+    @ApiModelProperty(value = "是否需要升级名称:字典 dis_yes_no")
+    private String needUpgradeName;
+
     @ApiModelProperty(value = "应用预案名称")
     private String planName;
 

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisHomeVO.java

@@ -18,7 +18,7 @@ import java.util.Map;
  * <ul>
  * <li>今日接报 / 较昨日:按 reportTime 落在今日 / 昨日的事件数;</li>
  * <li>处置中:status=2,其中等级为 Ⅰ级(紧急) 的单独计数;</li>
- * <li>已解除:status=3;处置率 = (已解除 + 处置中) / 全部事件 × 100;</li>
+ * <li>已解除:status=4;处置率 = (已解除 + 处置中) / 全部事件 × 100;</li>
  * <li>可用预案:dis_plan.status=2(已通过);已发布:存在 verStatus=1(当前版本)的预案版本数;</li>
  * <li>类型分布:近 7 日按 event_type 分组(type 为字典编码,前端 useDict 翻译);</li>
  * <li>等级占比:已解除单列一段,其余按 event_level(1紧急/2较大/3一般)分组。</li>

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisUserOptionVO.java

@@ -26,7 +26,7 @@ public class DisUserOptionVO implements Serializable {
     @ApiModelProperty("联系电话;选中后前端自动带出")
     private String phonenumber;
 
-    @ApiModelProperty("所属部门ID;前端据它把处置人候选限定在所选责任单位内")
+    @ApiModelProperty("用户所属部门ID")
     private Long deptId;
 
     @ApiModelProperty("所属部门名称")

+ 23 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventAuditDTO.java

@@ -0,0 +1,23 @@
+package com.zksy.dispatch.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/** 待验收事件最终核验入参。 */
+@Data
+@ApiModel("事件最终核验入参")
+public class EventAuditDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "核验结果:true 已解除,false 未解除", required = true)
+    private Boolean resolved;
+
+    @ApiModelProperty("核验意见")
+    private String auditOpinion;
+
+    @ApiModelProperty("附件URL,多个逗号分隔")
+    private String attachUrl;
+}

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventReleaseDTO.java

@@ -8,7 +8,7 @@ import java.io.Serializable;
 
 /**
  * 事件解除入参
- * 状态 2 处置中 → 3 已解除。
+ * 最终核验通过后,状态 3 待验收 → 4 已解除。
  */
 @Data
 @ApiModel(value = "事件解除入参", description = "事件解除入参")

+ 0 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventUrgeRowVO.java

@@ -53,9 +53,6 @@ public class EventUrgeRowVO implements Serializable {
     @ApiModelProperty(value = "责任人电话(初判核实录入的 handler_phone;催办弹窗展示用)")
     private String handlerPhone;
 
-    @ApiModelProperty(value = "责任部门")
-    private String department;
-
     @ApiModelProperty(value = "催办次数")
     private Long urgeCount;
 

+ 2 - 8
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventVerifyDTO.java

@@ -8,7 +8,7 @@ import java.io.Serializable;
 
 /**
  * 初判核实入参
- * 真实性判定为「真实」时填写类别、等级、处置单位及可选处置人;
+ * 真实性判定为「真实」时填写类别、等级、处置人及核验人;
  * 判定为「不真实」时隐藏业务处置字段,可附带核实意见,事件直接结束。
  */
 @Data
@@ -32,18 +32,12 @@ public class EventVerifyDTO implements Serializable {
     @ApiModelProperty(value = "处置人")
     private String handler;
 
-    @ApiModelProperty(value = "处置人用户ID(sys_user.user_id);须属于处置单位或其下级部门")
+    @ApiModelProperty(value = "处置人用户ID(sys_user.user_id);须为有效系统用户")
     private Long handlerId;
 
     @ApiModelProperty(value = "兼容旧客户端;当前核实流程不再使用")
     private String handlerPhone;
 
-    @ApiModelProperty(value = "处置单位名称")
-    private String department;
-
-    @ApiModelProperty(value = "处置单位部门ID")
-    private Long deptId;
-
     @ApiModelProperty(value = "核验人;为空时取当前登录用户")
     private String verifier;
 

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanApproveDTO.java

@@ -19,4 +19,7 @@ public class PlanApproveDTO implements Serializable {
 
     @ApiModelProperty(value = "审批意见;不通过时为驳回原因(必填)")
     private String approveNote;
+
+    @ApiModelProperty(value = "审核结果:true-通过 false-驳回", required = true)
+    private Boolean approved;
 }

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanPageDTO.java

@@ -19,6 +19,9 @@ public class PlanPageDTO implements Serializable {
     @ApiModelProperty(value = "预案编号(模糊)")
     private String planNo;
 
+    @ApiModelProperty(value = "预案编号或名称统一关键字(模糊)")
+    private String keyword;
+
     @ApiModelProperty(value = "预案名称(模糊)")
     private String planName;
 

+ 8 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/enums/EventLogOperTypeEnum.java

@@ -30,12 +30,16 @@ public enum EventLogOperTypeEnum {
     START_DISPOSE(5, "开始处置"),
     /** 处置跟踪,可多次记录 */
     TRACK(6, "处置跟踪"),
-    /** 事件解除 */
+    /** 历史事件解除 */
     RELEASE(7, "事件解除"),
     /** 工作催办 */
     URGE(8, "工作催办"),
     /** 事件信息修改 */
-    UPDATE(9, "信息修改");
+    UPDATE(9, "信息修改"),
+    /** 处置人提交核验 */
+    SUBMIT_VERIFY(10, "提交核验"),
+    /** 待验收后的最终核验 */
+    AUDIT(11, "最终核验");
 
     private final Integer code;
 
@@ -72,6 +76,7 @@ public enum EventLogOperTypeEnum {
                 || Objects.equals(code, VERIFY.code)
                 || Objects.equals(code, VERIFY_FALSE.code)
                 || Objects.equals(code, START_DISPOSE.code)
-                || Objects.equals(code, RELEASE.code);
+                || Objects.equals(code, AUDIT.code)
+                || Objects.equals(code, SUBMIT_VERIFY.code);
     }
 }

+ 13 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/enums/EventStatusEnum.java

@@ -11,7 +11,7 @@ import java.util.stream.Collectors;
 
 /**
  * 应急事件状态枚举(dis_event.status)
- * 主流程:0 待核实 → 1 已核实 → 2 处置中 → 3 已解除;旁路 4 不实事件(终态)。
+ * 主流程:0 待核实 → 1 已核实 → 2 处置中 → 3 待验收 → 4 已解除;旁路 5 不实事件(终态)。
  * 转接、报送、处置跟踪、工作催办只写 dis_event_log,不改变事件状态。
  * @author zengy
  */
@@ -25,10 +25,12 @@ public enum EventStatusEnum {
     VERIFIED(1, "已核实"),
     /** 已开始处置 */
     DISPOSING(2, "处置中"),
+    /** 处置人提交核验,等待指定核验人最终确认 */
+    PENDING_ACCEPTANCE(3, "待验收"),
     /** 处置完成并解除,终态 */
-    RELEASED(3, "已解除"),
+    RELEASED(4, "已解除"),
     /** 初判核实判定为不实,终态 */
-    FALSE_EVENT(4, "不实事件");
+    FALSE_EVENT(5, "不实事件");
 
     private final Integer code;
 
@@ -40,7 +42,7 @@ public enum EventStatusEnum {
 
     /** 流转中(未办结)状态 */
     private static final List<Integer> ACTIVE_CODES = Collections.unmodifiableList(
-            Arrays.asList(PENDING_VERIFY.code, VERIFIED.code, DISPOSING.code));
+            Arrays.asList(PENDING_VERIFY.code, VERIFIED.code, DISPOSING.code, PENDING_ACCEPTANCE.code));
 
     /** 允许编辑 / 删除的状态:待核实 */
     private static final List<Integer> EDITABLE_CODES = Collections.unmodifiableList(
@@ -112,11 +114,16 @@ public enum EventStatusEnum {
         return Objects.equals(code, DISPOSING.code);
     }
 
-    /** 可解除:仅处置中 */
-    public static boolean canRelease(Integer code) {
+    /** 处置人提交核验:仅处置中 */
+    public static boolean canSubmitVerify(Integer code) {
         return Objects.equals(code, DISPOSING.code);
     }
 
+    /** 指定核验人最终核验:仅待验收 */
+    public static boolean canAudit(Integer code) {
+        return Objects.equals(code, PENDING_ACCEPTANCE.code);
+    }
+
     /** 可转接 / 报送:已核实之后(含处置中) */
     public static boolean canTransfer(Integer code) {
         return code != null && (Objects.equals(code, VERIFIED.code) || Objects.equals(code, DISPOSING.code));

+ 2 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DispatchUserMapper.java

@@ -22,8 +22,7 @@ public interface DispatchUserMapper {
 
     /**
      * 选人下拉:全部启用状态的系统用户(昵称 + 联系电话 + 所属部门ID/名称)
-     * 「初判核实」责任处置人由此选择,选中后前端据 phonenumber 自动带出联系电话,
-     * 并据 deptId 把候选限定在所选责任单位内。
+     * 「初判核实」处置人及核验人由此选择,部门名称仅用于区分候选用户。
      */
     @Select("SELECT u.user_id, u.nick_name, u.phonenumber, u.dept_id, d.dept_name "
             + "FROM app_user.sys_user u "
@@ -34,8 +33,7 @@ public interface DispatchUserMapper {
 
     /**
      * 选部门下拉:全部正常状态的部门(ID + 父ID + 名称 + 显示顺序)
-     * 「初判核实」责任单位由此选择(原为手填);前端据 parentId 建树,
-     * 提交时取 deptName 写入 dis_event.department。
+     * 通用部门选择器可据 parentId 建树;事件核实不再依赖部门选择。
      */
     @Select("SELECT d.dept_id, d.parent_id, d.dept_name, d.order_num "
             + "FROM app_user.sys_dept d "

+ 15 - 14
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventService.java

@@ -16,14 +16,14 @@ import com.zksy.dispatch.dto.EventUrgeDTO;
 import com.zksy.dispatch.dto.EventUrgePageDTO;
 import com.zksy.dispatch.dto.EventUrgeRowVO;
 import com.zksy.dispatch.dto.EventVerifyDTO;
-import com.zksy.dispatch.dto.EventReleaseDTO;
+import com.zksy.dispatch.dto.EventAuditDTO;
 
 import java.util.List;
 
 /**
  * 应急事件统一档案 Service(全生命周期闭环)
- * 生命周期:接报登记(0 待核实)→ 初判核实(1 已核实 / 4 不实事件)
- * → 开始处置(2 处置中)→ 事件解除(3 已解除)。
+ * 生命周期:接报登记(0 待核实)→ 初判核实(1 已核实 / 5 不实事件)
+ * → 开始处置(2 处置中)→ 提交核验(3 待验收)→ 核验通过(4 已解除)。
  * 转接、报送、处置跟踪、工作催办都是「动作留痕」:只追加 dis_event_log,
  * 不改变事件状态。
  */
@@ -49,15 +49,15 @@ public interface DisEventService extends IService<DisEvent> {
      */
     EventStatsVO stats(DisEventPageDTO dto);
 
-    /**
-     * 责任处置人候选用户列表(启用状态的系统用户:昵称 + 联系电话 + 所属部门)
-     * 「初判核实」责任处置人选择用;选中后前端据 phonenumber 自动带出联系电话。
-     */
-    List<DisUserOptionVO> listUserOptions();
+    /** 初判核实处置人候选:有效且具备 dispatch:event:handle 权限的用户。 */
+    List<DisUserOptionVO> listHandlerOptions();
+
+    /** 初判核实核验人候选:有效且具备 dispatch:event:audit 权限的用户。 */
+    List<DisUserOptionVO> listVerifierOptions();
 
     /**
      * 责任单位候选部门列表(正常状态的 sys_dept,前端据 parentId 建树)
-     * 「初判核实」责任单位选择用;提交时取部门名称写入 dis_event.department。
+     * 通用部门选择器数据,不作为事件核实的提交参数或处置人范围约束。
      */
     List<DisDeptOptionVO> listDeptOptions();
 
@@ -77,7 +77,7 @@ public interface DisEventService extends IService<DisEvent> {
     void deleteEvent(Long id);
 
     /**
-     * 初判核实:0 待核实 → 1 已核实(真实)或 4 不实事件(不实),写 operType=1/2 日志
+     * 初判核实:0 待核实 → 1 已核实(真实)或 5 不实事件(不实),写 operType=1/2 日志
      */
     void verify(Long id, EventVerifyDTO dto);
 
@@ -101,10 +101,11 @@ public interface DisEventService extends IService<DisEvent> {
      */
     void track(EventTrackDTO dto);
 
-    /**
-     * 事件解除:2 处置中 → 3 已解除,写 operType=7 日志
-     */
-    void release(Long id, EventReleaseDTO dto);
+    /** 处置人提交核验:2 处置中 → 3 待验收。 */
+    void submitVerify(EventTrackDTO dto);
+
+    /** 指定核验人最终核验:3 待验收 → 4 已解除或 2 处置中。 */
+    void audit(Long id, EventAuditDTO dto);
 
     /**
      * 工作催办:只追加 operType=8 日志,不改状态

+ 127 - 55
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventServiceImpl.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.common.exception.ServiceException;
+import com.zksy.common.utils.DictUtils;
+import com.zksy.common.utils.SecurityUtils;
 import com.zksy.dispatch.domain.DisEvent;
 import com.zksy.dispatch.domain.DisEventLog;
 import com.zksy.dispatch.domain.DisPlan;
@@ -21,7 +23,7 @@ import com.zksy.dispatch.dto.EventUrgeDTO;
 import com.zksy.dispatch.dto.EventUrgePageDTO;
 import com.zksy.dispatch.dto.EventUrgeRowVO;
 import com.zksy.dispatch.dto.EventVerifyDTO;
-import com.zksy.dispatch.dto.EventReleaseDTO;
+import com.zksy.dispatch.dto.EventAuditDTO;
 import com.zksy.dispatch.support.DispatchAttachUrlSupport;
 import com.zksy.dispatch.dto.SseMessageDto;
 import com.zksy.dispatch.enums.EventLogOperTypeEnum;
@@ -36,6 +38,7 @@ import com.zksy.dispatch.service.DispatchNotifyService;
 import com.zksy.dispatch.support.DispatchNumberSupport;
 import com.zksy.dispatch.support.DispatchUserSupport;
 import com.zksy.dispatch.support.SseEmitterManager;
+import com.zksy.system.service.ISysMenuService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
@@ -92,6 +95,9 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
     @Autowired
     private DispatchUserMapper userMapper;
 
+    @Autowired
+    private ISysMenuService sysMenuService;
+
     // ==================== 查询 ====================
 
     @Override
@@ -157,6 +163,10 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
         }
         DisEventVO vo = toVO(event, loadUserNames(Collections.singletonList(event)));
         List<DisEventLog> logs = eventLogService.listByEventId(id);
+        logs.forEach(item -> {
+            item.setTrendName(dictName("dis_event_trend", item.getTrend()));
+            item.setNeedUpgradeName(dictName("dis_yes_no", item.getNeedUpgrade() == null ? null : item.getNeedUpgrade().toString()));
+        });
         vo.setLogs(logs);
         long urgeCount = 0L;
         for (DisEventLog item : logs) {
@@ -180,8 +190,36 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
     }
 
     @Override
-    public List<DisUserOptionVO> listUserOptions() {
-        return userMapper.selectUserOptions();
+    public List<DisUserOptionVO> listHandlerOptions() {
+        return listUsersWithPermission("dispatch:event:handle");
+    }
+
+    @Override
+    public List<DisUserOptionVO> listVerifierOptions() {
+        return listUsersWithPermission("dispatch:event:audit");
+    }
+
+    private List<DisUserOptionVO> listUsersWithPermission(String permission) {
+        return userMapper.selectUserOptions().stream()
+                .filter(user -> userHasPermission(user.getUserId(), permission))
+                .collect(Collectors.toList());
+    }
+
+    private boolean userHasPermission(Long userId, String permission) {
+        if (userId == null) return false;
+        if (SecurityUtils.isAdmin(userId)) return true;
+        java.util.Set<String> permissions = sysMenuService.selectMenuPermsByUserId(userId);
+        return permissions != null && SecurityUtils.hasPermi(permissions, permission);
+    }
+
+    private DisUserOptionVO requireUserWithPermission(Long userId, String permission, String label) {
+        DisUserOptionVO user = userMapper.selectUserOptions().stream()
+                .filter(option -> userId != null && userId.equals(option.getUserId()))
+                .findFirst().orElseThrow(() -> new ServiceException(label + "必须从有效系统用户中选择"));
+        if (!userHasPermission(userId, permission)) {
+            throw new ServiceException(label + "未配置对应事件操作权限");
+        }
+        return user;
     }
 
     @Override
@@ -350,7 +388,6 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
             row.setHandler(resolveUserName(userNames, event.getHandlerId(), event.getHandler()));
             row.setHandlerId(event.getHandlerId());
             row.setHandlerPhone(event.getHandlerPhone());
-            row.setDepartment(event.getDepartment());
             List<DisEventLog> urges = urgeMap.get(event.getId());
             row.setUrgeCount(CollectionUtils.isEmpty(urges) ? 0L : (long) urges.size());
             if (!CollectionUtils.isEmpty(urges)) {
@@ -465,16 +502,22 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
             if (StringUtils.isBlank(dto.getEventLevel())) {
                 throw new ServiceException("请选择事件等级");
             }
+            if (dto.getHandlerId() == null) {
+                throw new ServiceException("请选择处置人");
+            }
+            if (dto.getVerifierId() == null && DispatchUserSupport.currentUserId() == null) {
+                throw new ServiceException("请选择核验人");
+            }
         }
 
         LocalDateTime now = LocalDateTime.now();
         db.setVerifyResult(dto.getVerifyResult());
-        Long verifierId = dto.getVerifierId() == null ? DispatchUserSupport.currentUserId() : dto.getVerifierId();
+        Long verifierId = falseEvent ? DispatchUserSupport.currentUserId()
+                : (dto.getVerifierId() == null ? DispatchUserSupport.currentUserId() : dto.getVerifierId());
         String verifierName = DispatchUserSupport.currentUserName();
-        if (dto.getVerifierId() != null) {
-            DisUserOptionVO verifier = userMapper.selectUserOptions().stream()
-                    .filter(user -> dto.getVerifierId().equals(user.getUserId()))
-                    .findFirst().orElseThrow(() -> new ServiceException("核验人必须从有效系统用户中选择"));
+        if (!falseEvent) {
+            DisUserOptionVO verifier = requireUserWithPermission(
+                    verifierId, "dispatch:event:audit", "核验人");
             verifierName = verifier.getNickName();
         }
         db.setVerifier(verifierName);
@@ -486,43 +529,15 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
         } else {
             db.setEventCategory(dto.getEventCategory());
             db.setEventLevel(dto.getEventLevel());
-            Long deptId = dto.getDeptId();
-            if (deptId == null || StringUtils.isBlank(dto.getDepartment())) {
-                throw new ServiceException("请选择处置单位");
-            }
-            List<DisDeptOptionVO> departments = userMapper.selectDeptOptions();
-            DisDeptOptionVO selectedDept = departments.stream()
-                    .filter(dept -> deptId.equals(dept.getDeptId()))
-                    .findFirst().orElseThrow(() -> new ServiceException("处置单位无效"));
-            db.setDepartment(selectedDept.getDeptName());
-
-            // 处置人可选;如指定,必须属于所选单位或其下级部门。
+            // 处置人从有效系统用户中选择,不再依赖处置单位。
             Long handlerId = dto.getHandlerId();
             if (handlerId == null && StringUtils.isNotBlank(dto.getHandler())) {
                 handlerId = userMapper.selectUserIdByName(dto.getHandler());
             }
             if (handlerId != null) {
                 Long selectedHandlerId = handlerId;
-                DisUserOptionVO handler = userMapper.selectUserOptions().stream()
-                        .filter(user -> selectedHandlerId.equals(user.getUserId()))
-                        .findFirst().orElseThrow(() -> new ServiceException("处置人必须从有效系统用户中选择"));
-                Long handlerDeptId = handler.getDeptId();
-                boolean inScope = false;
-                for (int depth = 0; handlerDeptId != null && !Objects.equals(handlerDeptId, 0L)
-                        && depth <= departments.size(); depth++) {
-                    if (deptId.equals(handlerDeptId)) {
-                        inScope = true;
-                        break;
-                    }
-                    Long currentDeptId = handlerDeptId;
-                    DisDeptOptionVO currentDept = departments.stream()
-                            .filter(dept -> currentDeptId.equals(dept.getDeptId()))
-                            .findFirst().orElse(null);
-                    handlerDeptId = currentDept == null ? null : currentDept.getParentId();
-                }
-                if (!inScope) {
-                    throw new ServiceException("处置人须属于所选处置单位或其下级部门");
-                }
+                DisUserOptionVO handler = requireUserWithPermission(
+                        selectedHandlerId, "dispatch:event:handle", "处置人");
                 db.setHandler(handler.getNickName());
                 db.setHandlerId(selectedHandlerId);
                 db.setHandlerPhone(null);
@@ -589,6 +604,7 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
         if (!EventStatusEnum.canStartDispose(db.getStatus())) {
             throw new ServiceException("当前状态【" + EventStatusEnum.getDescByCode(db.getStatus()) + "】不可开始处置");
         }
+        requireHandler(db, "开始处置");
         Integer statusFrom = db.getStatus();
         LocalDateTime now = LocalDateTime.now();
         db.setStatus(EventStatusEnum.DISPOSING.getCode());
@@ -610,6 +626,7 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
         if (!EventStatusEnum.canTrack(db.getStatus())) {
             throw new ServiceException("当前状态【" + EventStatusEnum.getDescByCode(db.getStatus()) + "】不可跟踪");
         }
+        requireHandler(db, "跟踪");
         LocalDateTime now = LocalDateTime.now();
         db.setLastVisitTime(now);
         if (dto.getNextVisitTime() != null) {
@@ -633,29 +650,68 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void release(Long id, EventReleaseDTO dto) {
-        if (StringUtils.isBlank(dto.getReleaseReason())) {
-            throw new ServiceException("请选择解除原因");
+    public void submitVerify(EventTrackDTO dto) {
+        DisEvent db = requireForUpdate(dto.getEventId());
+        if (!EventStatusEnum.canSubmitVerify(db.getStatus())) {
+            throw new ServiceException("当前状态【" + EventStatusEnum.getDescByCode(db.getStatus()) + "】不可提交核验");
+        }
+        requireHandler(db, "提交核验");
+        Integer statusFrom = db.getStatus();
+        LocalDateTime now = LocalDateTime.now();
+        db.setStatus(EventStatusEnum.PENDING_ACCEPTANCE.getCode());
+        db.setUpdateUserId(DispatchUserSupport.currentUserId());
+        db.setUpdateTime(now);
+        updateById(db);
+
+        DisEventLog log = buildLog(db, EventLogOperTypeEnum.SUBMIT_VERIFY, statusFrom, db.getStatus());
+        log.setOperDesc(StringUtils.defaultIfBlank(dto.getOperDesc(), "处置人提交核验"));
+        log.setAttachUrl(attachUrlSupport.toRelative(dto.getAttachUrl()));
+        eventLogService.append(log);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void audit(Long id, EventAuditDTO dto) {
+        if (dto.getResolved() == null) {
+            throw new ServiceException("请选择核验结果");
         }
         DisEvent db = requireForUpdate(id);
-        if (!EventStatusEnum.canRelease(db.getStatus())) {
-            throw new ServiceException("当前状态【" + EventStatusEnum.getDescByCode(db.getStatus()) + "】不可解除");
+        if (!EventStatusEnum.canAudit(db.getStatus())) {
+            throw new ServiceException("当前状态【" + EventStatusEnum.getDescByCode(db.getStatus()) + "】不可核验");
+        }
+        Long currentUserId = DispatchUserSupport.currentUserId();
+        if (currentUserId == null || !Objects.equals(currentUserId, db.getVerifierId())) {
+            throw new ServiceException("仅指定核验人可以执行最终核验");
         }
         Integer statusFrom = db.getStatus();
         LocalDateTime now = LocalDateTime.now();
-        db.setStatus(EventStatusEnum.RELEASED.getCode());
-        db.setReleaseTime(now);
-        db.setReleaseReason(dto.getReleaseReason());
-        db.setReleaseNote(dto.getReleaseNote());
-        db.setUpdateUserId(DispatchUserSupport.currentUserId());
+        if (Boolean.TRUE.equals(dto.getResolved())) {
+            db.setStatus(EventStatusEnum.RELEASED.getCode());
+            db.setReleaseTime(now);
+            db.setReleaseNote(dto.getAuditOpinion());
+        } else {
+            db.setStatus(EventStatusEnum.DISPOSING.getCode());
+            db.setReleaseTime(null);
+            db.setReleaseReason(null);
+            db.setReleaseNote(null);
+        }
+        db.setUpdateUserId(currentUserId);
         db.setUpdateTime(now);
         updateById(db);
 
-        DisEventLog log = buildLog(db, EventLogOperTypeEnum.RELEASE, statusFrom, db.getStatus());
-        log.setOperDesc(StringUtils.defaultIfBlank(dto.getReleaseNote(), dto.getReleaseReason()));
+        DisEventLog log = buildLog(db, EventLogOperTypeEnum.AUDIT, statusFrom, db.getStatus());
+        log.setOperDesc(StringUtils.defaultIfBlank(dto.getAuditOpinion(), Boolean.TRUE.equals(dto.getResolved()) ? "核验结果:已解除" : "核验结果:未解除,继续处置"));
+        log.setAttachUrl(attachUrlSupport.toRelative(dto.getAttachUrl()));
         eventLogService.append(log);
     }
 
+    private void requireHandler(DisEvent event, String operation) {
+        Long currentUserId = DispatchUserSupport.currentUserId();
+        if (currentUserId == null || !Objects.equals(currentUserId, event.getHandlerId())) {
+            throw new ServiceException("仅指定处置人可以" + operation);
+        }
+    }
+
     // ==================== 工作催办(留痕,不改状态) ====================
 
     @Override
@@ -706,13 +762,17 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
      */
     private LambdaQueryWrapper<DisEvent> buildWrapper(DisEventPageDTO dto) {
         LambdaQueryWrapper<DisEvent> wrapper = new LambdaQueryWrapper<>();
-        wrapper.like(StringUtils.isNotBlank(dto.getEventNo()), DisEvent::getEventNo, dto.getEventNo());
-        wrapper.like(StringUtils.isNotBlank(dto.getDescription()), DisEvent::getDescription, dto.getDescription());
+        if (StringUtils.isNotBlank(dto.getKeyword())) {
+            wrapper.and(w -> w.like(DisEvent::getEventNo, dto.getKeyword())
+                    .or().like(DisEvent::getDescription, dto.getKeyword()));
+        } else {
+            wrapper.like(StringUtils.isNotBlank(dto.getEventNo()), DisEvent::getEventNo, dto.getEventNo());
+            wrapper.like(StringUtils.isNotBlank(dto.getDescription()), DisEvent::getDescription, dto.getDescription());
+        }
         wrapper.eq(StringUtils.isNotBlank(dto.getEventCategory()), DisEvent::getEventCategory, dto.getEventCategory());
         wrapper.eq(StringUtils.isNotBlank(dto.getEventLevel()), DisEvent::getEventLevel, dto.getEventLevel());
         wrapper.eq(StringUtils.isNotBlank(dto.getEventSource()), DisEvent::getEventSource, dto.getEventSource());
         wrapper.like(StringUtils.isNotBlank(dto.getLocation()), DisEvent::getLocation, dto.getLocation());
-        wrapper.like(StringUtils.isNotBlank(dto.getDepartment()), DisEvent::getDepartment, dto.getDepartment());
         wrapper.like(StringUtils.isNotBlank(dto.getHandler()), DisEvent::getHandler, dto.getHandler());
         if (!CollectionUtils.isEmpty(dto.getStatusList())) {
             wrapper.in(DisEvent::getStatus, dto.getStatusList());
@@ -750,11 +810,23 @@ public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
         vo.setVerifier(resolveUserName(userNames, event.getVerifierId(), event.getVerifier()));
         vo.setHandler(resolveUserName(userNames, event.getHandlerId(), event.getHandler()));
         vo.setStatusName(EventStatusEnum.getDescByCode(event.getStatus()));
+        vo.setEventSourceName(dictName("dis_event_source", event.getEventSource()));
+        vo.setChannelName(dictName("dis_event_channel", event.getChannel()));
+        vo.setEventCategoryName(dictName("dis_event_category", event.getEventCategory()));
+        vo.setEventLevelName(dictName("dis_event_level", event.getEventLevel()));
+        vo.setReleaseReasonName(dictName("dis_event_release_reason", event.getReleaseReason()));
+        vo.setNeedUpgradeName(dictName("dis_yes_no", event.getNeedUpgrade() == null ? null : event.getNeedUpgrade().toString()));
         vo.setEditable(EventStatusEnum.canEdit(event.getStatus()));
         vo.setElapsed(formatElapsed(event.getCreateTime()));
         return vo;
     }
 
+    private String dictName(String type, String value) {
+        if (StringUtils.isBlank(value)) return null;
+        String label = DictUtils.getDictLabel(type, value);
+        return StringUtils.isBlank(label) ? value : label;
+    }
+
     /**
      * 批量取出事件列表里出现的人员ID,一次查询回昵称映射,避免逐行查库。
      * @return 用户ID -> 昵称;列表里没有任何人员ID时返回空映射

+ 16 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisPlanServiceImpl.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.DictUtils;
 import com.zksy.dispatch.domain.DisPlan;
 import com.zksy.dispatch.domain.DisPlanLog;
 import com.zksy.dispatch.domain.DisPlanVersion;
@@ -78,8 +79,13 @@ public class DisPlanServiceImpl extends ServiceImpl<DisPlanMapper, DisPlan>
     public Page<DisPlan> findByPage(PlanPageDTO dto) {
         Page<DisPlan> page = new Page<>(dto.getPageNum(), dto.getPageSize());
         LambdaQueryWrapper<DisPlan> wrapper = new LambdaQueryWrapper<>();
-        wrapper.like(StringUtils.isNotBlank(dto.getPlanNo()), DisPlan::getPlanNo, dto.getPlanNo());
-        wrapper.like(StringUtils.isNotBlank(dto.getPlanName()), DisPlan::getPlanName, dto.getPlanName());
+        if (StringUtils.isNotBlank(dto.getKeyword())) {
+            wrapper.and(w -> w.like(DisPlan::getPlanNo, dto.getKeyword())
+                    .or().like(DisPlan::getPlanName, dto.getKeyword()));
+        } else {
+            wrapper.like(StringUtils.isNotBlank(dto.getPlanNo()), DisPlan::getPlanNo, dto.getPlanNo());
+            wrapper.like(StringUtils.isNotBlank(dto.getPlanName()), DisPlan::getPlanName, dto.getPlanName());
+        }
         wrapper.eq(StringUtils.isNotBlank(dto.getPlanType()), DisPlan::getPlanType, dto.getPlanType());
         wrapper.eq(StringUtils.isNotBlank(dto.getPlanLevel()), DisPlan::getPlanLevel, dto.getPlanLevel());
         wrapper.eq(dto.getStatus() != null, DisPlan::getStatus, dto.getStatus());
@@ -398,11 +404,19 @@ public class DisPlanServiceImpl extends ServiceImpl<DisPlanMapper, DisPlan>
      */
     private void fillTransient(DisPlan plan, boolean hasCandidate) {
         plan.setStatusName(PlanStatusEnum.getDescByCode(plan.getStatus()));
+        plan.setPlanTypeName(dictName("dis_plan_type", plan.getPlanType()));
+        plan.setPlanLevelName(dictName("dis_plan_level", plan.getPlanLevel()));
         plan.setEditable(PlanStatusEnum.canEdit(plan.getStatus()) || PlanStatusEnum.canRevise(plan.getStatus()));
         plan.setSubmittable(PlanStatusEnum.canSubmit(plan.getStatus()));
         plan.setHasCandidate(hasCandidate);
     }
 
+    private String dictName(String type, String value) {
+        if (StringUtils.isBlank(value)) return null;
+        String label = DictUtils.getDictLabel(type, value);
+        return StringUtils.isBlank(label) ? value : label;
+    }
+
     /**
      * 把本预案当前生效的版本降为历史;keepVersionId 为将要提升为生效的候选版本
      */

+ 23 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/support/DispatchAttachUrlSupport.java

@@ -4,6 +4,9 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
 
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
 /**
  * 调度附件地址支撑:数据库统一存相对路径(如 /dispatch/2026/10/16/x.docx),
  * 读出口拼 minio.endpoint + bucket 返回完整 URL。
@@ -25,6 +28,16 @@ public class DispatchAttachUrlSupport {
      * 库内存储值 → 可访问完整 URL(展示/下载出口统一走这里)
      */
     public String toAccessibleUrl(String stored) {
+        if (!StringUtils.hasText(stored)) {
+            return stored;
+        }
+        return Arrays.stream(stored.split(","))
+                .map(this::toAccessibleSingle)
+                .filter(StringUtils::hasText)
+                .collect(Collectors.joining(","));
+    }
+
+    private String toAccessibleSingle(String stored) {
         if (!StringUtils.hasText(stored)) {
             return stored;
         }
@@ -42,6 +55,16 @@ public class DispatchAttachUrlSupport {
      * 前端/入参值 → 库内相对路径(写入口统一走这里)
      */
     public String toRelative(String input) {
+        if (!StringUtils.hasText(input)) {
+            return input;
+        }
+        return Arrays.stream(input.split(","))
+                .map(this::toRelativeSingle)
+                .filter(StringUtils::hasText)
+                .collect(Collectors.joining(","));
+    }
+
+    private String toRelativeSingle(String input) {
         if (!StringUtils.hasText(input)) {
             return input;
         }

+ 64 - 16
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/EngineeringFacility.java

@@ -2,6 +2,7 @@ package com.zksy.drainage.domain;
 
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModelProperty;
@@ -9,47 +10,94 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.Date;
 
 /**
- * 工程设施基础数据
+ * 工程设施基础数据(闸门 / 调蓄设施 / 泵站)
+ * <p>一个工程设施在地图上是一个点要素,直接落 WGS84 经纬度。</p>
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-@TableName(value = "psgw_engineering_facility", schema = "app_user")
+@TableName(value = "drainage_engineering_facility", schema = "app_user")
 public class EngineeringFacility {
 
-    @TableId(value = "id", type = IdType.ASSIGN_UUID)
-    @ApiModelProperty("主键ID")
-    private String id;
+    /** 设施类型:闸门(字典 drainage_facility_type) */
+    public static final String TYPE_GATE = "0";
 
-    @ApiModelProperty("监测点名称/设施名称")
-    private String name;
+    /** 设施类型:调蓄设施 */
+    public static final String TYPE_STORAGE = "1";
 
-    @ApiModelProperty("行政区域")
-    private String area;
+    /** 设施类型:泵站 */
+    public static final String TYPE_PUMP = "2";
 
-    @ApiModelProperty("设施类型(闸门/调蓄设施/泵站)")
+    @TableId(value = "facility_id", type = IdType.ASSIGN_UUID)
+    @ApiModelProperty("设施ID")
+    private String facilityId;
+
+    @ApiModelProperty("设施编码")
+    private String facilityCode;
+
+    @ApiModelProperty("设施名称")
+    private String facilityName;
+
+    @ApiModelProperty("设施类型(0闸门 1调蓄设施 2泵站)")
     private String facilityType;
 
-    @ApiModelProperty("数据时间")
-    private String time;
+    @ApiModelProperty("设施等级")
+    private String facilityLevel;
+
+    @ApiModelProperty("管理单位")
+    private String managementUnit;
+
+    @ApiModelProperty("位置描述")
+    private String location;
+
+    @ApiModelProperty("WGS84 经度")
+    private BigDecimal longitude;
+
+    @ApiModelProperty("WGS84 纬度")
+    private BigDecimal latitude;
 
-    @ApiModelProperty("标记数量(自动计算)")
-    private Integer pointCount;
+    @ApiModelProperty("运行状态(0正常 1检修 2停用 3故障)")
+    private String status;
 
-    @ApiModelProperty("坐标数据JSON")
-    private String points;
+    @ApiModelProperty("建设日期")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date buildDate;
+
+    @ApiModelProperty("设计流量(m³/s)")
+    private BigDecimal designFlow;
+
+    @ApiModelProperty("设计扬程(m)")
+    private BigDecimal designHead;
+
+    @ApiModelProperty("调蓄容积(m³)")
+    private BigDecimal storageCapacity;
 
     @ApiModelProperty("关联设备ID(多个用逗号分隔)")
     private String equipmentId;
 
+    @ApiModelProperty("备注")
+    private String remark;
+
+    @ApiModelProperty("创建人")
+    private String createBy;
+
     @ApiModelProperty("创建时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime createTime;
 
+    @ApiModelProperty("更新人")
+    private String updateBy;
+
     @ApiModelProperty("更新时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime updateTime;
+
+    @TableLogic(value = "0", delval = "2")
+    @ApiModelProperty("删除标志(0正常 2删除)")
+    private String delFlag;
 }

+ 64 - 21
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/WaterloggingPoint.java

@@ -3,6 +3,7 @@ package com.zksy.drainage.domain;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModelProperty;
@@ -14,49 +15,91 @@ import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 /**
- * 易积水点管理
+ * 易积水点台账
+ * <p>一个易积水点在地图上就是一个位置,直接落 WGS84 经纬度;
+ * 关联的是视频摄像头(equipment_base.equipment_id),不是通用监测设备。</p>
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-@TableName(value = "psgw_waterlogging_point", schema = "app_user")
+@TableName(value = "drainage_waterlogging_point", schema = "app_user")
 public class WaterloggingPoint {
 
-    @TableId(value = "id", type = IdType.ASSIGN_UUID)
-    @ApiModelProperty("主键ID")
-    private String id;
+    @TableId(value = "point_id", type = IdType.ASSIGN_UUID)
+    @ApiModelProperty("点位ID")
+    private String pointId;
 
-    @ApiModelProperty("监测点名称")
-    private String name;
+    @ApiModelProperty("点位编码")
+    private String pointCode;
 
-    @ApiModelProperty("行政区域")
-    private String area;
+    @ApiModelProperty("点位名称")
+    private String pointName;
 
-    @ApiModelProperty("数据时间")
-    private String time;
+    @ApiModelProperty("所属区域")
+    private String district;
 
-    @ApiModelProperty("易积水点数量(自动计算)")
-    private Integer waterloggingCount;
+    @ApiModelProperty("位置描述")
+    private String location;
 
-    @ApiModelProperty("易积水点标记坐标JSON")
-    private String waterloggingPoints;
+    @ApiModelProperty("WGS84 经度")
+    private BigDecimal longitude;
 
-    @ApiModelProperty("积水量")
-    @TableField("water_volume")
-    private BigDecimal waterVolume;
+    @ApiModelProperty("WGS84 纬度")
+    private BigDecimal latitude;
 
-    @ApiModelProperty("积水程度(0未积水,1轻度积水,2中度积水,3严重积水)")
+    @ApiModelProperty("积水程度(0未积水 1轻度 2中度 3严重)")
     @TableField("waterlogging_level")
     private Integer waterloggingLevel;
 
-    @ApiModelProperty("关联设备ID(多个用逗号分隔)")
-    private String equipmentId;
+    @ApiModelProperty("积水深度(cm)")
+    @TableField("water_depth")
+    private BigDecimal waterDepth;
+
+    @ApiModelProperty("积水量(m³)")
+    @TableField("water_volume")
+    private BigDecimal waterVolume;
+
+    @ApiModelProperty("积水面积(m²)")
+    @TableField("water_area")
+    private BigDecimal waterArea;
+
+    @ApiModelProperty("风险等级(0低 1中 2高)")
+    @TableField("risk_level")
+    private String riskLevel;
+
+    @ApiModelProperty("责任单位")
+    @TableField("duty_unit")
+    private String dutyUnit;
+
+    @ApiModelProperty("责任人")
+    @TableField("duty_person")
+    private String dutyPerson;
+
+    @ApiModelProperty("联系电话")
+    @TableField("duty_phone")
+    private String dutyPhone;
+
+    @ApiModelProperty("关联摄像头ID(多个用逗号分隔)")
+    private String cameraIds;
+
+    @ApiModelProperty("备注")
+    private String remark;
+
+    @ApiModelProperty("创建人")
+    private String createBy;
 
     @ApiModelProperty("创建时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime createTime;
 
+    @ApiModelProperty("更新人")
+    private String updateBy;
+
     @ApiModelProperty("更新时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime updateTime;
+
+    @TableLogic(value = "0", delval = "2")
+    @ApiModelProperty("删除标志(0正常 2删除)")
+    private String delFlag;
 }

+ 43 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/dto/EngineeringFacilityPageInDTO.java

@@ -0,0 +1,43 @@
+package com.zksy.drainage.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 工程设施查询入参(分页查询与地图查询共用)
+ */
+@Data
+@ApiModel(value = "工程设施查询入参", description = "工程设施查询入参")
+public class EngineeringFacilityPageInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "设施编码")
+    private String facilityCode;
+
+    @ApiModelProperty(value = "设施名称")
+    private String facilityName;
+
+    @ApiModelProperty(value = "设施类型(0闸门 1调蓄设施 2泵站)")
+    private String facilityType;
+
+    @ApiModelProperty(value = "设施等级")
+    private String facilityLevel;
+
+    @ApiModelProperty(value = "管理单位")
+    private String managementUnit;
+
+    @ApiModelProperty(value = "位置描述")
+    private String location;
+
+    @ApiModelProperty(value = "运行状态(0正常 1检修 2停用 3故障)")
+    private String status;
+
+    @ApiModelProperty(value = "当前记录起始索引")
+    private Long pageNum;
+
+    @ApiModelProperty(value = "每页显示记录数")
+    private Long pageSize;
+}

+ 37 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/dto/WaterloggingPointPageInDTO.java

@@ -0,0 +1,37 @@
+package com.zksy.drainage.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 易积水点查询入参(分页查询与地图查询共用)
+ */
+@Data
+@ApiModel(value = "易积水点查询入参", description = "易积水点查询入参")
+public class WaterloggingPointPageInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "点位编码")
+    private String pointCode;
+
+    @ApiModelProperty(value = "点位名称")
+    private String pointName;
+
+    @ApiModelProperty(value = "所属区域")
+    private String district;
+
+    @ApiModelProperty(value = "积水程度(0未积水 1轻度 2中度 3严重)")
+    private Integer waterloggingLevel;
+
+    @ApiModelProperty(value = "风险等级(0低 1中 2高)")
+    private String riskLevel;
+
+    @ApiModelProperty(value = "当前记录起始索引")
+    private Long pageNum;
+
+    @ApiModelProperty(value = "每页显示记录数")
+    private Long pageSize;
+}

+ 8 - 7
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/mapper/DrainageVideoMapper.java

@@ -10,16 +10,16 @@ import org.apache.ibatis.annotations.*;
 public interface DrainageVideoMapper {
     String VIEW = "SELECT e.equipment_id AS video_id,e.equipment_code AS device_code,e.equipment_name AS device_name," +
         "e.district,e.equipment_location AS location,e.longitude,e.latitude,v.source_type," +
-        "w.id AS point_id,w.name AS point_name,(f.user_id IS NOT NULL) AS favorite " +
+        "w.point_id AS point_id,w.point_name AS point_name,(f.user_id IS NOT NULL) AS favorite " +
         "FROM app_user.equipment_base e JOIN app_user.equipment_video_binding v ON v.equipment_id=e.equipment_id " +
-        "LEFT JOIN app_user.psgw_waterlogging_point w ON STRPOS(CONCAT(',',REPLACE(COALESCE(w.equipment_id,''),' ',''),','),CONCAT(',',e.equipment_id,','))>0 " +
+        "LEFT JOIN app_user.drainage_waterlogging_point w ON STRPOS(CONCAT(',',REPLACE(COALESCE(w.camera_ids,''),' ',''),','),CONCAT(',',e.equipment_id,','))>0 " +
         "LEFT JOIN app_user.drainage_video_favorite f ON f.equipment_id=e.equipment_id AND f.user_id=#{userId} ";
     String FILTER = "WHERE e.equipment_type_id=20 AND e.del_flag='0' " +
         "<if test='query != null and query != &quot;&quot;'>AND (e.equipment_name LIKE CONCAT('%',#{query},'%') OR e.equipment_code LIKE CONCAT('%',#{query},'%'))</if> " +
         "<if test='district != null'>AND e.district=#{district}</if> " +
-        "<if test='pointId != null and pointId != &quot;&quot;'>AND w.id=#{pointId}</if> " +
-        "<if test='unbound == true'>AND w.id IS NULL</if> " +
-        "<if test='unbound == false'>AND w.id IS NOT NULL</if> " +
+        "<if test='pointId != null and pointId != &quot;&quot;'>AND w.point_id=#{pointId}</if> " +
+        "<if test='unbound == true'>AND w.point_id IS NULL</if> " +
+        "<if test='unbound == false'>AND w.point_id IS NOT NULL</if> " +
         "<if test='favoriteOnly == true'>AND f.user_id IS NOT NULL</if> " +
         "<if test='sourceType != null and sourceType != &quot;&quot;'>AND v.source_type=#{sourceType}</if> " +
         "ORDER BY e.create_time DESC,e.equipment_id";
@@ -32,7 +32,8 @@ public interface DrainageVideoMapper {
     @Select(VIEW + "WHERE e.equipment_type_id=20 AND e.del_flag='0' ORDER BY e.district,e.equipment_id")
     List<DrainageVideoResource> all(@Param("userId") Long userId);
 
-    @Select("SELECT id,name,area FROM app_user.psgw_waterlogging_point ORDER BY name,id")
+    // 别名保持 id/name/area,兼容视频分组页面既有的消费方
+    @Select("SELECT point_id AS id,point_name AS name,district AS area FROM app_user.drainage_waterlogging_point WHERE del_flag='0' ORDER BY point_name,point_id")
     List<Map<String,Object>> points();
 
     @Insert("INSERT INTO app_user.drainage_video_favorite(user_id,equipment_id) VALUES(#{userId},#{videoId}) ON CONFLICT(user_id,equipment_id) DO NOTHING")
@@ -41,6 +42,6 @@ public interface DrainageVideoMapper {
     @Delete("DELETE FROM app_user.drainage_video_favorite WHERE user_id=#{userId} AND equipment_id=#{videoId}")
     int unfavorite(@Param("userId") Long userId,@Param("videoId") String videoId);
 
-    @Update("UPDATE app_user.psgw_waterlogging_point SET equipment_id=#{equipmentIds},update_time=CURRENT_TIMESTAMP WHERE id=#{pointId}")
+    @Update("UPDATE app_user.drainage_waterlogging_point SET camera_ids=#{equipmentIds},update_time=CURRENT_TIMESTAMP WHERE point_id=#{pointId}")
     int updatePoint(@Param("pointId") String pointId,@Param("equipmentIds") String equipmentIds);
 }

+ 5 - 5
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageVideoService.java

@@ -73,19 +73,19 @@ public class DrainageVideoService {
         List<WaterloggingPoint> all=points.selectList(null);
         WaterloggingPoint target=null;
         if(pointId!=null&&!pointId.isBlank()) {
-            target=all.stream().filter(p->pointId.equals(p.getId())).findFirst().orElseThrow(()->new ServiceException("易积水点不存在"));
+            target=all.stream().filter(p->pointId.equals(p.getPointId())).findFirst().orElseThrow(()->new ServiceException("易积水点不存在"));
             for(WaterloggingPoint point:all) {
-                if(!pointId.equals(point.getId())&&ids(point.getEquipmentId()).contains(videoId))
+                if(!pointId.equals(point.getPointId())&&ids(point.getCameraIds()).contains(videoId))
                     throw new ServiceException("视频已关联其他易积水点,请先解除关联");
             }
             Map<String,String> occupied=binding.findBoundEquipment(DrainageEquipmentBindingService.TYPE_WATERLOGGING,pointId);
             if(occupied.containsKey(videoId))throw new ServiceException("视频已被其他位置关联:"+occupied.get(videoId));
-            Set<String> merged=ids(target.getEquipmentId());merged.add(videoId);
+            Set<String> merged=ids(target.getCameraIds());merged.add(videoId);
             videos.updatePoint(pointId,String.join(",",merged));
         } else {
             for(WaterloggingPoint point:all) {
-                Set<String> existing=ids(point.getEquipmentId());
-                if(existing.remove(videoId))videos.updatePoint(point.getId(),String.join(",",existing));
+                Set<String> existing=ids(point.getCameraIds());
+                if(existing.remove(videoId))videos.updatePoint(point.getPointId(),String.join(",",existing));
             }
         }
     }

+ 13 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/EngineeringFacilityService.java

@@ -3,8 +3,10 @@ package com.zksy.drainage.service;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.drainage.domain.EngineeringFacility;
+import com.zksy.drainage.dto.EngineeringFacilityPageInDTO;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 工程设施基础数据 Service接口
@@ -14,8 +16,17 @@ public interface EngineeringFacilityService extends IService<EngineeringFacility
     /**
      * 分页查询
      */
-    Page<EngineeringFacility> findByPage(long pageNum, long pageSize,
-                                         String name, String area, String facilityType);
+    Page<EngineeringFacility> findByPage(EngineeringFacilityPageInDTO dto);
+
+    /**
+     * 地图数据:与分页查询同一套筛选条件,不分页,仅返回带坐标的记录
+     */
+    List<EngineeringFacility> listForMap(EngineeringFacilityPageInDTO dto);
+
+    /**
+     * 筛选下拉项:设施类型、等级、管理单位的库内去重值
+     */
+    Map<String, List<String>> getFilterOptions();
 
     /**
      * 新增

+ 3 - 27
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/PipeNetworkInfoService.java

@@ -4,10 +4,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.drainage.domain.PipeNetworkInfo;
 
-import java.util.List;
-
 /**
  * 排水管网基础数据 Service接口
+ * <p>
+ * 旧表 psgw_pipe_network_info 已停止写入,仅保留分页查询供风险分析类页面使用;
+ * 新增 / 修改 / 删除 / 批量删除 / 设备绑定方法对应端点已移除,故一并清理。
  */
 public interface PipeNetworkInfoService extends IService<PipeNetworkInfo> {
 
@@ -16,29 +17,4 @@ public interface PipeNetworkInfoService extends IService<PipeNetworkInfo> {
      */
     Page<PipeNetworkInfo> findByPage(long pageNum, long pageSize,
                                      String name, String area, String networkType);
-
-    /**
-     * 新增
-     */
-    boolean saveEntity(PipeNetworkInfo entity);
-
-    /**
-     * 修改
-     */
-    boolean updateEntity(PipeNetworkInfo entity);
-
-    /**
-     * 删除
-     */
-    boolean removeEntity(String id);
-
-    /**
-     * 批量删除
-     */
-    boolean removeBatchEntity(List<String> ids);
-
-    /**
-     * 绑定设备到管网
-     */
-    boolean bindEquipment(String id, List<String> equipmentIds);
 }

+ 15 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/WaterloggingPointService.java

@@ -3,8 +3,10 @@ package com.zksy.drainage.service;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.drainage.domain.WaterloggingPoint;
+import com.zksy.drainage.dto.WaterloggingPointPageInDTO;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 易积水点管理 Service接口
@@ -14,7 +16,17 @@ public interface WaterloggingPointService extends IService<WaterloggingPoint> {
     /**
      * 分页查询
      */
-    Page<WaterloggingPoint> findByPage(long pageNum, long pageSize, String name, String area);
+    Page<WaterloggingPoint> findByPage(WaterloggingPointPageInDTO dto);
+
+    /**
+     * 地图数据:与分页查询同一套筛选条件,不分页,仅返回带坐标的记录
+     */
+    List<WaterloggingPoint> listForMap(WaterloggingPointPageInDTO dto);
+
+    /**
+     * 筛选下拉项:所属区域、积水程度的库内去重值
+     */
+    Map<String, List<String>> getFilterOptions();
 
     /**
      * 新增
@@ -37,7 +49,7 @@ public interface WaterloggingPointService extends IService<WaterloggingPoint> {
     boolean removeBatchWithCheck(List<String> ids);
 
     /**
-     * 绑定设备到易积水点
+     * 关联摄像头到易积水点(整体替换,传空列表表示解除全部关联)
      */
-    boolean bindEquipment(String id, List<String> equipmentIds);
+    boolean bindCameras(String pointId, List<String> cameraIds);
 }

+ 12 - 10
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageEquipmentBindingServiceImpl.java

@@ -57,19 +57,22 @@ public class DrainageEquipmentBindingServiceImpl implements DrainageEquipmentBin
         }
         for (EngineeringFacility item : engineeringFacilityMapper.selectList(
                 new LambdaQueryWrapper<EngineeringFacility>().select(
-                        EngineeringFacility::getId, EngineeringFacility::getName,
+                        EngineeringFacility::getFacilityId, EngineeringFacility::getFacilityName,
                         EngineeringFacility::getFacilityType, EngineeringFacility::getEquipmentId))) {
             // 泵站设施的 equipment_id 保存的是泵站ID,不属于监测设备关联。
-            if ("泵站".equals(item.getFacilityType())) {
+            if (EngineeringFacility.TYPE_PUMP.equals(item.getFacilityType())
+                    || "泵站".equals(item.getFacilityType())) {
                 continue;
             }
-            addBindings(result, TYPE_FACILITY, item.getId(), item.getName(), item.getEquipmentId(),
+            addBindings(result, TYPE_FACILITY, item.getFacilityId(), item.getFacilityName(), item.getEquipmentId(),
                     excludeType, excludeId, "工程设施");
         }
+        // 易积水点关联的是摄像头(equipment_base.equipment_type_id=20),同样参与占用校验
         for (WaterloggingPoint item : waterloggingPointMapper.selectList(
                 new LambdaQueryWrapper<WaterloggingPoint>().select(
-                        WaterloggingPoint::getId, WaterloggingPoint::getName, WaterloggingPoint::getEquipmentId))) {
-            addBindings(result, TYPE_WATERLOGGING, item.getId(), item.getName(), item.getEquipmentId(),
+                        WaterloggingPoint::getPointId, WaterloggingPoint::getPointName,
+                        WaterloggingPoint::getCameraIds))) {
+            addBindings(result, TYPE_WATERLOGGING, item.getPointId(), item.getPointName(), item.getCameraIds(),
                     excludeType, excludeId, "易积水点");
         }
         return result;
@@ -117,16 +120,15 @@ public class DrainageEquipmentBindingServiceImpl implements DrainageEquipmentBin
             case TYPE_FACILITY:
                 EngineeringFacility facility = engineeringFacilityMapper.selectById(sourceId);
                 if (facility == null) return false;
-                if ("泵站".equals(facility.getFacilityType())) {
+                if (EngineeringFacility.TYPE_PUMP.equals(facility.getFacilityType())
+                        || "泵站".equals(facility.getFacilityType())) {
                     throw new ServiceException("泵站设施请使用关联泵站功能");
                 }
                 facility.setEquipmentId(value);
                 return engineeringFacilityMapper.updateById(facility) > 0;
             case TYPE_WATERLOGGING:
-                WaterloggingPoint waterloggingPoint = waterloggingPointMapper.selectById(sourceId);
-                if (waterloggingPoint == null) return false;
-                waterloggingPoint.setEquipmentId(value);
-                return waterloggingPointMapper.updateById(waterloggingPoint) > 0;
+                // 易积水点关联的是摄像头,走 WaterloggingPointService.bindCameras,不再共用设备关联入口
+                throw new ServiceException("易积水点关联的是摄像头,请使用关联摄像头功能");
             default:
                 return false;
         }

+ 64 - 14
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/EngineeringFacilityServiceImpl.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.common.exception.ServiceException;
 import com.zksy.drainage.domain.EngineeringFacility;
+import com.zksy.drainage.dto.EngineeringFacilityPageInDTO;
 import com.zksy.drainage.mapper.EngineeringFacilityMapper;
 import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.EngineeringFacilityService;
@@ -14,7 +15,10 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -29,21 +33,67 @@ public class EngineeringFacilityServiceImpl extends ServiceImpl<EngineeringFacil
     private DrainageEquipmentBindingService equipmentBindingService;
 
     @Override
-    public Page<EngineeringFacility> findByPage(long pageNum, long pageSize,
-                                                String name, String area, String facilityType) {
+    public Page<EngineeringFacility> findByPage(EngineeringFacilityPageInDTO dto) {
+        EngineeringFacilityPageInDTO query = dto == null ? new EngineeringFacilityPageInDTO() : dto;
+        long pageNum = query.getPageNum() == null || query.getPageNum() < 1 ? 1 : query.getPageNum();
+        long pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : query.getPageSize();
         Page<EngineeringFacility> page = new Page<>(pageNum, pageSize);
+        return this.page(page, buildWrapper(query));
+    }
+
+    @Override
+    public List<EngineeringFacility> listForMap(EngineeringFacilityPageInDTO dto) {
+        EngineeringFacilityPageInDTO query = dto == null ? new EngineeringFacilityPageInDTO() : dto;
+        LambdaQueryWrapper<EngineeringFacility> wrapper = buildWrapper(query);
+        wrapper.isNotNull(EngineeringFacility::getLongitude)
+                .isNotNull(EngineeringFacility::getLatitude)
+                .orderByAsc(EngineeringFacility::getFacilityCode);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public Map<String, List<String>> getFilterOptions() {
+        Map<String, List<String>> options = new LinkedHashMap<>();
+        options.put("facilityType", distinctValues(EngineeringFacility::getFacilityType));
+        options.put("facilityLevel", distinctValues(EngineeringFacility::getFacilityLevel));
+        options.put("managementUnit", distinctValues(EngineeringFacility::getManagementUnit));
+        options.put("status", distinctValues(EngineeringFacility::getStatus));
+        return options;
+    }
+
+    /**
+     * 分页与地图共用同一套筛选条件,避免「列表筛了、地图没筛」的口径漂移。
+     */
+    private LambdaQueryWrapper<EngineeringFacility> buildWrapper(EngineeringFacilityPageInDTO query) {
         LambdaQueryWrapper<EngineeringFacility> wrapper = new LambdaQueryWrapper<>();
-        if (name != null && !name.isEmpty()) {
-            wrapper.like(EngineeringFacility::getName, name);
-        }
-        if (area != null && !area.isEmpty()) {
-            wrapper.like(EngineeringFacility::getArea, area);
-        }
-        if (facilityType != null && !facilityType.isEmpty()) {
-            wrapper.eq(EngineeringFacility::getFacilityType, facilityType);
-        }
+        wrapper.like(hasText(query.getFacilityCode()), EngineeringFacility::getFacilityCode, query.getFacilityCode());
+        wrapper.like(hasText(query.getFacilityName()), EngineeringFacility::getFacilityName, query.getFacilityName());
+        wrapper.eq(hasText(query.getFacilityType()), EngineeringFacility::getFacilityType, query.getFacilityType());
+        wrapper.eq(hasText(query.getFacilityLevel()), EngineeringFacility::getFacilityLevel, query.getFacilityLevel());
+        wrapper.like(hasText(query.getManagementUnit()), EngineeringFacility::getManagementUnit, query.getManagementUnit());
+        wrapper.like(hasText(query.getLocation()), EngineeringFacility::getLocation, query.getLocation());
+        wrapper.eq(hasText(query.getStatus()), EngineeringFacility::getStatus, query.getStatus());
         wrapper.orderByDesc(EngineeringFacility::getCreateTime);
-        return this.page(page, wrapper);
+        return wrapper;
+    }
+
+    private boolean hasText(String value) {
+        return value != null && !value.trim().isEmpty();
+    }
+
+    private List<String> distinctValues(com.baomidou.mybatisplus.core.toolkit.support.SFunction<EngineeringFacility, ?> column) {
+        List<EngineeringFacility> rows = this.list(new LambdaQueryWrapper<EngineeringFacility>()
+                .select(column)
+                .isNotNull(column)
+                .groupBy(column));
+        List<String> values = new ArrayList<>(rows.size());
+        for (EngineeringFacility row : rows) {
+            Object value = column.apply(row);
+            if (value != null && !String.valueOf(value).trim().isEmpty()) {
+                values.add(String.valueOf(value).trim());
+            }
+        }
+        return values.stream().distinct().sorted().collect(Collectors.toList());
     }
 
     @Override
@@ -57,7 +107,7 @@ public class EngineeringFacilityServiceImpl extends ServiceImpl<EngineeringFacil
     @Override
     @Transactional
     public boolean updateEntity(EngineeringFacility entity) {
-        String id = entity.getId();
+        String id = entity.getFacilityId();
         if (this.getById(id) == null) {
             throw new ServiceException("工程设施不存在,无法修改:" + id);
         }
@@ -85,7 +135,7 @@ public class EngineeringFacilityServiceImpl extends ServiceImpl<EngineeringFacil
             throw new ServiceException("批量删除失败:所有ID均不存在");
         }
         List<String> existIds = existing.stream()
-                .map(EngineeringFacility::getId)
+                .map(EngineeringFacility::getFacilityId)
                 .collect(Collectors.toList());
         List<String> invalidIds = ids.stream()
                 .filter(id -> !existIds.contains(id))

+ 3 - 67
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/PipeNetworkInfoServiceImpl.java

@@ -1,33 +1,25 @@
 package com.zksy.drainage.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.zksy.common.exception.ServiceException;
 import com.zksy.drainage.domain.PipeNetworkInfo;
 import com.zksy.drainage.mapper.PipeNetworkInfoMapper;
-import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.PipeNetworkInfoService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * 排水管网基础数据 ServiceImpl
+ * <p>
+ * 旧表 psgw_pipe_network_info 已停止写入,仅保留分页查询。
+ * 增删改与设备绑定实现已随后端端点清理一并移除。
  */
 @Slf4j
 @Service
 public class PipeNetworkInfoServiceImpl extends ServiceImpl<PipeNetworkInfoMapper, PipeNetworkInfo>
         implements PipeNetworkInfoService {
 
-    @org.springframework.beans.factory.annotation.Autowired
-    private DrainageEquipmentBindingService equipmentBindingService;
-
     @Override
     public Page<PipeNetworkInfo> findByPage(long pageNum, long pageSize,
                                             String name, String area, String networkType) {
@@ -45,60 +37,4 @@ public class PipeNetworkInfoServiceImpl extends ServiceImpl<PipeNetworkInfoMappe
         wrapper.orderByDesc(PipeNetworkInfo::getCreateTime);
         return this.page(page, wrapper);
     }
-
-    @Override
-    @Transactional
-    public boolean saveEntity(PipeNetworkInfo entity) {
-        entity.setCreateTime(LocalDateTime.now());
-        entity.setUpdateTime(LocalDateTime.now());
-        return this.save(entity);
-    }
-
-    @Override
-    @Transactional
-    public boolean updateEntity(PipeNetworkInfo entity) {
-        String id = entity.getId();
-        if (this.getById(id) == null) {
-            throw new ServiceException("排水管网不存在,无法修改:" + id);
-        }
-        entity.setUpdateTime(LocalDateTime.now());
-        return this.updateById(entity);
-    }
-
-    @Override
-    @Transactional
-    public boolean removeEntity(String id) {
-        if (this.getById(id) == null) {
-            throw new ServiceException("排水管网不存在:" + id);
-        }
-        return this.baseMapper.deleteById(id) > 0;
-    }
-
-    @Override
-    @Transactional
-    public boolean removeBatchEntity(List<String> ids) {
-        if (CollectionUtils.isEmpty(ids)) {
-            throw new ServiceException("批量删除失败:ID列表不能为空");
-        }
-        List<PipeNetworkInfo> existing = this.listByIds(ids);
-        if (existing.isEmpty()) {
-            throw new ServiceException("批量删除失败:所有ID均不存在");
-        }
-        List<String> existIds = existing.stream()
-                .map(PipeNetworkInfo::getId)
-                .collect(Collectors.toList());
-        List<String> invalidIds = ids.stream()
-                .filter(id -> !existIds.contains(id))
-                .collect(Collectors.toList());
-        if (!invalidIds.isEmpty()) {
-            log.warn("批量删除中存在无效排水管网ID:{}", invalidIds);
-        }
-        return this.removeByIds(existIds);
-    }
-
-    @Override
-    public boolean bindEquipment(String id, List<String> equipmentIds) {
-        return equipmentBindingService.bindEquipment(
-                DrainageEquipmentBindingService.TYPE_PIPE_NETWORK, id, equipmentIds);
-    }
 }

+ 139 - 36
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/WaterloggingPointServiceImpl.java

@@ -2,18 +2,25 @@ package com.zksy.drainage.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.common.exception.ServiceException;
 import com.zksy.drainage.domain.WaterloggingPoint;
+import com.zksy.drainage.dto.WaterloggingPointPageInDTO;
 import com.zksy.drainage.mapper.WaterloggingPointMapper;
-import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.WaterloggingPointService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -24,57 +31,144 @@ import java.util.stream.Collectors;
 public class WaterloggingPointServiceImpl extends ServiceImpl<WaterloggingPointMapper, WaterloggingPoint>
         implements WaterloggingPointService {
 
-    @org.springframework.beans.factory.annotation.Autowired
-    private DrainageEquipmentBindingService equipmentBindingService;
-
     @org.springframework.beans.factory.annotation.Autowired
     private com.zksy.drainage.mapper.DrainageBindingLockMapper bindingLock;
 
     @Override
-    public Page<WaterloggingPoint> findByPage(long pageNum, long pageSize, String name, String area) {
-        Page<WaterloggingPoint> page = new Page<>(pageNum, pageSize);
+    public Page<WaterloggingPoint> findByPage(WaterloggingPointPageInDTO dto) {
+        WaterloggingPointPageInDTO query = dto == null ? new WaterloggingPointPageInDTO() : dto;
+        long pageNum = query.getPageNum() == null || query.getPageNum() < 1 ? 1 : query.getPageNum();
+        long pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : query.getPageSize();
+        return this.page(new Page<>(pageNum, pageSize), buildWrapper(query));
+    }
+
+    @Override
+    public List<WaterloggingPoint> listForMap(WaterloggingPointPageInDTO dto) {
+        WaterloggingPointPageInDTO query = dto == null ? new WaterloggingPointPageInDTO() : dto;
+        LambdaQueryWrapper<WaterloggingPoint> wrapper = buildWrapper(query);
+        wrapper.isNotNull(WaterloggingPoint::getLongitude)
+                .isNotNull(WaterloggingPoint::getLatitude)
+                .orderByAsc(WaterloggingPoint::getPointCode);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public Map<String, List<String>> getFilterOptions() {
+        Map<String, List<String>> options = new LinkedHashMap<>();
+        options.put("district", distinctValues(WaterloggingPoint::getDistrict));
+        options.put("waterloggingLevel", distinctValues(WaterloggingPoint::getWaterloggingLevel));
+        options.put("riskLevel", distinctValues(WaterloggingPoint::getRiskLevel));
+        return options;
+    }
+
+    /**
+     * 分页与地图共用同一套筛选条件,避免口径漂移。
+     */
+    private LambdaQueryWrapper<WaterloggingPoint> buildWrapper(WaterloggingPointPageInDTO query) {
         LambdaQueryWrapper<WaterloggingPoint> wrapper = new LambdaQueryWrapper<>();
-        if (name != null && !name.isEmpty()) {
-            wrapper.like(WaterloggingPoint::getName, name);
-        }
-        if (area != null && !area.isEmpty()) {
-            wrapper.like(WaterloggingPoint::getArea, area);
-        }
+        wrapper.like(hasText(query.getPointCode()), WaterloggingPoint::getPointCode, query.getPointCode());
+        wrapper.like(hasText(query.getPointName()), WaterloggingPoint::getPointName, query.getPointName());
+        wrapper.like(hasText(query.getDistrict()), WaterloggingPoint::getDistrict, query.getDistrict());
+        wrapper.eq(query.getWaterloggingLevel() != null,
+                WaterloggingPoint::getWaterloggingLevel, query.getWaterloggingLevel());
+        wrapper.eq(hasText(query.getRiskLevel()), WaterloggingPoint::getRiskLevel, query.getRiskLevel());
         wrapper.orderByDesc(WaterloggingPoint::getCreateTime);
-        return this.page(page, wrapper);
+        return wrapper;
+    }
+
+    private boolean hasText(String value) {
+        return value != null && !value.trim().isEmpty();
+    }
+
+    private List<String> distinctValues(SFunction<WaterloggingPoint, ?> column) {
+        List<WaterloggingPoint> rows = this.list(new LambdaQueryWrapper<WaterloggingPoint>()
+                .select(column)
+                .isNotNull(column)
+                .groupBy(column));
+        List<String> values = new ArrayList<>(rows.size());
+        for (WaterloggingPoint row : rows) {
+            Object value = column.apply(row);
+            if (value != null && !String.valueOf(value).trim().isEmpty()) {
+                values.add(String.valueOf(value).trim());
+            }
+        }
+        return values.stream().distinct().sorted().collect(Collectors.toList());
     }
 
     @Override
     @Transactional
     public boolean saveWithCheck(WaterloggingPoint entity) {
         bindingLock.lock();
-        String equipmentIds = entity.getEquipmentId();
-        entity.setEquipmentId("");
+        String cameraIds = entity.getCameraIds();
+        entity.setCameraIds(null);
+        entity.setCreateTime(LocalDateTime.now());
+        entity.setUpdateTime(LocalDateTime.now());
         if (!this.save(entity)) return false;
-        if (equipmentIds != null && !equipmentIds.isBlank())
-            equipmentBindingService.bindEquipment(DrainageEquipmentBindingService.TYPE_WATERLOGGING,
-                    entity.getId(), java.util.Arrays.asList(equipmentIds.split(",")));
-        return true;
+        return applyCameras(entity.getPointId(), cameraIds);
     }
 
     @Override
     @Transactional
     public boolean updateWithCheck(WaterloggingPoint entity) {
         bindingLock.lock();
-        String id = entity.getId();
-        if (this.getById(id) == null) {
-            throw new ServiceException("易积水点不存在,无法修改:" + id);
+        String pointId = entity.getPointId();
+        if (this.getById(pointId) == null) {
+            throw new ServiceException("易积水点不存在,无法修改:" + pointId);
         }
-        String equipmentIds = entity.getEquipmentId();
-        entity.setEquipmentId(null);
+        String cameraIds = entity.getCameraIds();
+        entity.setCameraIds(null);
+        entity.setUpdateTime(LocalDateTime.now());
         if (!this.updateById(entity)) return false;
-        if (equipmentIds != null && !equipmentIds.isBlank())
-            equipmentBindingService.bindEquipment(DrainageEquipmentBindingService.TYPE_WATERLOGGING,
-                    id, java.util.Arrays.asList(equipmentIds.split(",")));
-        else if (equipmentIds != null)
-            this.baseMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WaterloggingPoint>()
-                    .eq(WaterloggingPoint::getId, id).set(WaterloggingPoint::getEquipmentId, ""));
-        return true;
+        // cameraIds 传 null 表示本次不动关联;传空串表示解除全部关联
+        if (cameraIds == null) return true;
+        return applyCameras(pointId, cameraIds);
+    }
+
+    /**
+     * 整体替换某点位的摄像头关联,并校验摄像头是否已被其他点位占用。
+     */
+    private boolean applyCameras(String pointId, String cameraIds) {
+        List<String> normalized = parseIds(cameraIds);
+        checkCameraOccupation(pointId, normalized);
+        WaterloggingPoint patch = new WaterloggingPoint();
+        patch.setPointId(pointId);
+        patch.setCameraIds(String.join(",", normalized));
+        patch.setUpdateTime(LocalDateTime.now());
+        return this.updateById(patch);
+    }
+
+    @Override
+    @Transactional
+    public boolean bindCameras(String pointId, List<String> cameraIds) {
+        bindingLock.lock();
+        if (this.getById(pointId) == null) {
+            throw new ServiceException("易积水点不存在:" + pointId);
+        }
+        List<String> normalized = normalizeIds(cameraIds);
+        checkCameraOccupation(pointId, normalized);
+        WaterloggingPoint patch = new WaterloggingPoint();
+        patch.setPointId(pointId);
+        patch.setCameraIds(String.join(",", normalized));
+        patch.setUpdateTime(LocalDateTime.now());
+        return this.updateById(patch);
+    }
+
+    /** 摄像头只能挂在一个易积水点上,避免摄像头不在现场却出现在两个点位。 */
+    private void checkCameraOccupation(String pointId, List<String> cameraIds) {
+        if (cameraIds.isEmpty()) return;
+        List<WaterloggingPoint> others = this.list(new LambdaQueryWrapper<WaterloggingPoint>()
+                .ne(WaterloggingPoint::getPointId, pointId)
+                .isNotNull(WaterloggingPoint::getCameraIds));
+        for (WaterloggingPoint other : others) {
+            Set<String> occupied = parseIds(other.getCameraIds()).stream()
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+            for (String cameraId : cameraIds) {
+                if (occupied.contains(cameraId)) {
+                    String name = hasText(other.getPointName()) ? other.getPointName() : other.getPointId();
+                    throw new ServiceException("摄像头已被易积水点「" + name + "」关联,请先解除");
+                }
+            }
+        }
     }
 
     @Override
@@ -99,14 +193,23 @@ public class WaterloggingPointServiceImpl extends ServiceImpl<WaterloggingPointM
             throw new ServiceException("批量删除失败:所有ID均不存在");
         }
         List<String> existIds = existing.stream()
-                .map(WaterloggingPoint::getId)
+                .map(WaterloggingPoint::getPointId)
                 .collect(Collectors.toList());
         return this.removeByIds(existIds);
     }
 
-    @Override
-    public boolean bindEquipment(String id, List<String> equipmentIds) {
-        return equipmentBindingService.bindEquipment(
-                DrainageEquipmentBindingService.TYPE_WATERLOGGING, id, equipmentIds);
+    private List<String> parseIds(String value) {
+        if (value == null || value.trim().isEmpty()) return new ArrayList<>();
+        return normalizeIds(java.util.Arrays.asList(value.split(",")));
+    }
+
+    private List<String> normalizeIds(List<String> ids) {
+        if (ids == null) return new ArrayList<>();
+        return ids.stream()
+                .filter(java.util.Objects::nonNull)
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .distinct()
+                .collect(Collectors.toList());
     }
 }

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/system/mapper/SysUserMapper.java

@@ -71,6 +71,14 @@ public interface SysUserMapper
      */
     public SysUser selectUserByUserName(String userName);
 
+    /**
+     * 通过用户名或手机号码查询用户
+     *
+     * @param account 用户名或手机号码
+     * @return 用户对象信息
+     */
+    public SysUser selectUserByUserNameOrPhone(String account);
+
     /**
      * 通过用户ID查询用户
      * 

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/ISysUserService.java

@@ -66,6 +66,14 @@ public interface ISysUserService
      */
     public SysUser selectUserByUserName(String userName);
 
+    /**
+     * 通过用户名或手机号查询用户(支持手机号登录)
+     *
+     * @param account 用户名或手机号
+     * @return 用户对象信息
+     */
+    public SysUser selectUserByUserNameOrPhone(String account);
+
     /**
      * 通过用户ID查询用户
      * 

+ 91 - 142
pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/impl/SysUserServiceImpl.java

@@ -4,6 +4,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 import javax.validation.Validator;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -34,12 +35,11 @@ import com.zksy.system.service.ISysUserService;
 
 /**
  * 用户 业务层处理
- * 
+ *
  * @author zksy
  */
 @Service
-public class SysUserServiceImpl implements ISysUserService
-{
+public class SysUserServiceImpl implements ISysUserService {
     private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);
 
     @Autowired
@@ -68,25 +68,23 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 根据条件分页查询用户列表
-     * 
+     *
      * @param user 用户信息
      * @return 用户信息集合信息
      */
     @Override
     @DataScope(deptAlias = "d", userAlias = "u")
-    public List<SysUser> selectUserList(SysUser user)
-    {
+    public List<SysUser> selectUserList(SysUser user) {
         return userMapper.selectUserList(user);
     }
 
     /**
      * 查询全部有效用户(未删除且状态正常,不做数据权限过滤)
-     * 
+     *
      * @return 有效用户信息集合
      */
     @Override
-    public List<SysUser> selectAllEnabledUser()
-    {
+    public List<SysUser> selectAllEnabledUser() {
         return userMapper.selectAllEnabledUser();
     }
 
@@ -105,10 +103,8 @@ public class SysUserServiceImpl implements ISysUserService
      * @return 用户信息集合
      */
     @Override
-    public List<SysUser> selectEnabledUsersByIds(java.util.Collection<Long> userIds)
-    {
-        if (userIds == null || userIds.isEmpty())
-        {
+    public List<SysUser> selectEnabledUsersByIds(java.util.Collection<Long> userIds) {
+        if (userIds == null || userIds.isEmpty()) {
             return new java.util.ArrayList<>();
         }
         return userMapper.selectEnabledUsersByIds(userIds);
@@ -116,66 +112,65 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 根据条件分页查询已分配用户角色列表
-     * 
+     *
      * @param user 用户信息
      * @return 用户信息集合信息
      */
     @Override
     @DataScope(deptAlias = "d", userAlias = "u")
-    public List<SysUser> selectAllocatedList(SysUser user)
-    {
+    public List<SysUser> selectAllocatedList(SysUser user) {
         return userMapper.selectAllocatedList(user);
     }
 
     /**
      * 根据条件分页查询未分配用户角色列表
-     * 
+     *
      * @param user 用户信息
      * @return 用户信息集合信息
      */
     @Override
     @DataScope(deptAlias = "d", userAlias = "u")
-    public List<SysUser> selectUnallocatedList(SysUser user)
-    {
+    public List<SysUser> selectUnallocatedList(SysUser user) {
         return userMapper.selectUnallocatedList(user);
     }
 
     /**
      * 通过用户名查询用户
-     * 
+     *
      * @param userName 用户名
      * @return 用户对象信息
      */
     @Override
-    public SysUser selectUserByUserName(String userName)
-    {
+    public SysUser selectUserByUserName(String userName) {
         return userMapper.selectUserByUserName(userName);
     }
 
+    @Override
+    public SysUser selectUserByUserNameOrPhone(String account) {
+        return userMapper.selectUserByUserNameOrPhone(account);
+    }
+
     /**
      * 通过用户ID查询用户
-     * 
+     *
      * @param userId 用户ID
      * @return 用户对象信息
      */
     @Override
-    public SysUser selectUserById(Long userId)
-    {
+    public SysUser selectUserById(Long userId) {
         return userMapper.selectUserById(userId);
     }
 
     /**
      * 查询用户所属角色组
-     * 
+     *
      * @param userName 用户名
      * @return 结果
      */
     @Override
-    public String selectUserRoleGroup(String userName)
-    {
+    public String selectUserRoleGroup(String userName) {
         List<SysRole> list = roleMapper.selectRolesByUserName(userName);
-        if (CollectionUtils.isEmpty(list))
-        {
+        if (CollectionUtils.isEmpty(list)) {
             return StringUtils.EMPTY;
         }
         return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(","));
@@ -183,16 +178,14 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 查询用户所属岗位组
-     * 
+     *
      * @param userName 用户名
      * @return 结果
      */
     @Override
-    public String selectUserPostGroup(String userName)
-    {
+    public String selectUserPostGroup(String userName) {
         List<SysPost> list = postMapper.selectPostsByUserName(userName);
-        if (CollectionUtils.isEmpty(list))
-        {
+        if (CollectionUtils.isEmpty(list)) {
             return StringUtils.EMPTY;
         }
         return list.stream().map(SysPost::getPostName).collect(Collectors.joining(","));
@@ -200,17 +193,15 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 校验用户名称是否唯一
-     * 
+     *
      * @param user 用户信息
      * @return 结果
      */
     @Override
-    public boolean checkUserNameUnique(SysUser user)
-    {
+    public boolean checkUserNameUnique(SysUser user) {
         Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
         SysUser info = userMapper.checkUserNameUnique(user.getUserName());
-        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
-        {
+        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) {
             return UserConstants.NOT_UNIQUE;
         }
         return UserConstants.UNIQUE;
@@ -223,12 +214,10 @@ public class SysUserServiceImpl implements ISysUserService
      * @return
      */
     @Override
-    public boolean checkPhoneUnique(SysUser user)
-    {
+    public boolean checkPhoneUnique(SysUser user) {
         Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
         SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber());
-        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
-        {
+        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) {
             return UserConstants.NOT_UNIQUE;
         }
         return UserConstants.UNIQUE;
@@ -241,12 +230,10 @@ public class SysUserServiceImpl implements ISysUserService
      * @return
      */
     @Override
-    public boolean checkEmailUnique(SysUser user)
-    {
+    public boolean checkEmailUnique(SysUser user) {
         Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
         SysUser info = userMapper.checkEmailUnique(user.getEmail());
-        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
-        {
+        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) {
             return UserConstants.NOT_UNIQUE;
         }
         return UserConstants.UNIQUE;
@@ -254,33 +241,28 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 校验用户是否允许操作
-     * 
+     *
      * @param user 用户信息
      */
     @Override
-    public void checkUserAllowed(SysUser user)
-    {
-        if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin())
-        {
+    public void checkUserAllowed(SysUser user) {
+        if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) {
             throw new ServiceException("不允许操作超级管理员用户");
         }
     }
 
     /**
      * 校验用户是否有数据权限
-     * 
+     *
      * @param userId 用户id
      */
     @Override
-    public void checkUserDataScope(Long userId)
-    {
-        if (!SysUser.isAdmin(SecurityUtils.getUserId()))
-        {
+    public void checkUserDataScope(Long userId) {
+        if (!SysUser.isAdmin(SecurityUtils.getUserId())) {
             SysUser user = new SysUser();
             user.setUserId(userId);
             List<SysUser> users = SpringUtils.getAopProxy(this).selectUserList(user);
-            if (StringUtils.isEmpty(users))
-            {
+            if (StringUtils.isEmpty(users)) {
                 throw new ServiceException("没有权限访问用户数据!");
             }
         }
@@ -288,14 +270,13 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 新增保存用户信息
-     * 
+     *
      * @param user 用户信息
      * @return 结果
      */
     @Override
     @Transactional
-    public int insertUser(SysUser user)
-    {
+    public int insertUser(SysUser user) {
         // 新增用户信息
         int rows = userMapper.insertUser(user);
         // 新增用户岗位关联
@@ -307,26 +288,24 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 注册用户信息
-     * 
+     *
      * @param user 用户信息
      * @return 结果
      */
     @Override
-    public boolean registerUser(SysUser user)
-    {
+    public boolean registerUser(SysUser user) {
         return userMapper.insertUser(user) > 0;
     }
 
     /**
      * 修改保存用户信息
-     * 
+     *
      * @param user 用户信息
      * @return 结果
      */
     @Override
     @Transactional
-    public int updateUser(SysUser user)
-    {
+    public int updateUser(SysUser user) {
         Long userId = user.getUserId();
         // 删除用户与角色关联
         userRoleMapper.deleteUserRoleByUserId(userId);
@@ -341,104 +320,94 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 用户授权角色
-     * 
-     * @param userId 用户ID
+     *
+     * @param userId  用户ID
      * @param roleIds 角色组
      */
     @Override
     @Transactional
-    public void insertUserAuth(Long userId, Long[] roleIds)
-    {
+    public void insertUserAuth(Long userId, Long[] roleIds) {
         userRoleMapper.deleteUserRoleByUserId(userId);
         insertUserRole(userId, roleIds);
     }
 
     /**
      * 修改用户状态
-     * 
+     *
      * @param user 用户信息
      * @return 结果
      */
     @Override
-    public int updateUserStatus(SysUser user)
-    {
+    public int updateUserStatus(SysUser user) {
         return userMapper.updateUser(user);
     }
 
     /**
      * 修改用户基本信息
-     * 
+     *
      * @param user 用户信息
      * @return 结果
      */
     @Override
-    public int updateUserProfile(SysUser user)
-    {
+    public int updateUserProfile(SysUser user) {
         return userMapper.updateUser(user);
     }
 
     /**
      * 修改用户头像
-     * 
+     *
      * @param userId 用户ID
      * @param avatar 头像地址
      * @return 结果
      */
     @Override
-    public boolean updateUserAvatar(Long userId, String avatar)
-    {
+    public boolean updateUserAvatar(Long userId, String avatar) {
         return userMapper.updateUserAvatar(userId, avatar) > 0;
     }
 
     /**
      * 重置用户密码
-     * 
+     *
      * @param user 用户信息
      * @return 结果
      */
     @Override
-    public int resetPwd(SysUser user)
-    {
+    public int resetPwd(SysUser user) {
         return userMapper.updateUser(user);
     }
 
     /**
      * 重置用户密码
-     * 
-     * @param userId 用户ID
+     *
+     * @param userId   用户ID
      * @param password 密码
      * @return 结果
      */
     @Override
-    public int resetUserPwd(Long userId, String password)
-    {
+    public int resetUserPwd(Long userId, String password) {
         return userMapper.resetUserPwd(userId, password);
     }
 
     /**
      * 新增用户角色信息
-     * 
+     *
      * @param user 用户对象
      */
-    public void insertUserRole(SysUser user)
-    {
+    public void insertUserRole(SysUser user) {
         this.insertUserRole(user.getUserId(), user.getRoleIds());
     }
 
     /**
      * 新增用户岗位信息
-     * 
+     *
      * @param user 用户对象
      */
-    public void insertUserPost(SysUser user)
-    {
+    public void insertUserPost(SysUser user) {
         Long[] posts = user.getPostIds();
-        if (StringUtils.isNotEmpty(posts))
-        {
+        if (StringUtils.isNotEmpty(posts)) {
             // 新增用户与岗位管理
             List<SysUserPost> list = new ArrayList<SysUserPost>(posts.length);
-            for (Long postId : posts)
-            {
+            for (Long postId : posts) {
                 SysUserPost up = new SysUserPost();
                 up.setUserId(user.getUserId());
                 up.setPostId(postId);
@@ -450,18 +419,15 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 新增用户角色信息
-     * 
-     * @param userId 用户ID
+     *
+     * @param userId  用户ID
      * @param roleIds 角色组
      */
-    public void insertUserRole(Long userId, Long[] roleIds)
-    {
-        if (StringUtils.isNotEmpty(roleIds))
-        {
+    public void insertUserRole(Long userId, Long[] roleIds) {
+        if (StringUtils.isNotEmpty(roleIds)) {
             // 新增用户与角色管理
             List<SysUserRole> list = new ArrayList<SysUserRole>(roleIds.length);
-            for (Long roleId : roleIds)
-            {
+            for (Long roleId : roleIds) {
                 SysUserRole ur = new SysUserRole();
                 ur.setUserId(userId);
                 ur.setRoleId(roleId);
@@ -473,14 +439,13 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 通过用户ID删除用户
-     * 
+     *
      * @param userId 用户ID
      * @return 结果
      */
     @Override
     @Transactional
-    public int deleteUserById(Long userId)
-    {
+    public int deleteUserById(Long userId) {
         // 删除用户与角色关联
         userRoleMapper.deleteUserRoleByUserId(userId);
         // 删除用户与岗位表
@@ -490,16 +455,14 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 批量删除用户信息
-     * 
+     *
      * @param userIds 需要删除的用户ID
      * @return 结果
      */
     @Override
     @Transactional
-    public int deleteUserByIds(Long[] userIds)
-    {
-        for (Long userId : userIds)
-        {
+    public int deleteUserByIds(Long[] userIds) {
+        for (Long userId : userIds) {
             checkUserAllowed(new SysUser(userId));
             checkUserDataScope(userId);
         }
@@ -512,31 +475,26 @@ public class SysUserServiceImpl implements ISysUserService
 
     /**
      * 导入用户数据
-     * 
-     * @param userList 用户数据列表
+     *
+     * @param userList        用户数据列表
      * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
-     * @param operName 操作用户
+     * @param operName        操作用户
      * @return 结果
      */
     @Override
-    public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName)
-    {
-        if (StringUtils.isNull(userList) || userList.size() == 0)
-        {
+    public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName) {
+        if (StringUtils.isNull(userList) || userList.size() == 0) {
             throw new ServiceException("导入用户数据不能为空!");
         }
         int successNum = 0;
         int failureNum = 0;
         StringBuilder successMsg = new StringBuilder();
         StringBuilder failureMsg = new StringBuilder();
-        for (SysUser user : userList)
-        {
-            try
-            {
+        for (SysUser user : userList) {
+            try {
                 // 验证是否存在这个用户
                 SysUser u = userMapper.selectUserByUserName(user.getUserName());
-                if (StringUtils.isNull(u))
-                {
+                if (StringUtils.isNull(u)) {
                     BeanValidators.validateWithException(validator, user);
                     deptService.checkDeptDataScope(user.getDeptId());
                     String password = configService.selectConfigByKey("sys.user.initPassword");
@@ -545,9 +503,7 @@ public class SysUserServiceImpl implements ISysUserService
                     userMapper.insertUser(user);
                     successNum++;
                     successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
-                }
-                else if (isUpdateSupport)
-                {
+                } else if (isUpdateSupport) {
                     BeanValidators.validateWithException(validator, user);
                     checkUserAllowed(u);
                     checkUserDataScope(u.getUserId());
@@ -557,28 +513,21 @@ public class SysUserServiceImpl implements ISysUserService
                     userMapper.updateUser(user);
                     successNum++;
                     successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
-                }
-                else
-                {
+                } else {
                     failureNum++;
                     failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
                 }
-            }
-            catch (Exception e)
-            {
+            } catch (Exception e) {
                 failureNum++;
                 String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
                 failureMsg.append(msg + e.getMessage());
                 log.error(msg, e);
             }
         }
-        if (failureNum > 0)
-        {
+        if (failureNum > 0) {
             failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
             throw new ServiceException(failureMsg.toString());
-        }
-        else
-        {
+        } else {
             successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
         }
         return successMsg.toString();

+ 6 - 0
pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml

@@ -197,6 +197,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	    <include refid="selectUserVo"/>
 		where u.user_name = #{userName} and u.del_flag = '0'
 	</select>
+
+	<select id="selectUserByUserNameOrPhone" parameterType="String" resultMap="SysUserResult">
+	    <include refid="selectUserVo"/>
+		where (u.user_name = #{account} or u.phonenumber = #{account}) and u.del_flag = '0'
+		limit 1
+	</select>
 	
 	<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
 		<include refid="selectUserVo"/>

+ 46 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/EventStatusContractTest.java

@@ -0,0 +1,46 @@
+package com.zksy.dispatch;
+
+import com.zksy.dispatch.enums.EventStatusEnum;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class EventStatusContractTest {
+
+    @Test
+    void acceptanceCodeIsActiveAndAllowsFinalAuditOnly() {
+        assertEquals(EventStatusEnum.PENDING_ACCEPTANCE, EventStatusEnum.getByCode(3));
+        assertEquals("待验收", EventStatusEnum.getDescByCode(3));
+        assertTrue(EventStatusEnum.canAudit(3));
+        assertTrue(EventStatusEnum.isActive(3));
+        assertFalse(EventStatusEnum.isFinished(3));
+        assertFalse(EventStatusEnum.canSubmitVerify(3));
+        assertFalse(EventStatusEnum.canEdit(3));
+        assertFalse(EventStatusEnum.canTransfer(3));
+        assertFalse(EventStatusEnum.canTrack(3));
+    }
+
+    @Test
+    void releasedAndFalseCodesAreTerminalAndCannotBeAudited() {
+        assertEquals(EventStatusEnum.RELEASED, EventStatusEnum.getByCode(4));
+        assertEquals(EventStatusEnum.FALSE_EVENT, EventStatusEnum.getByCode(5));
+        assertEquals(Arrays.asList(4, 5), EventStatusEnum.finishedCodes());
+        for (int code : Arrays.asList(4, 5)) {
+            assertTrue(EventStatusEnum.isFinished(code));
+            assertFalse(EventStatusEnum.isActive(code));
+            assertFalse(EventStatusEnum.canAudit(code));
+            assertFalse(EventStatusEnum.canSubmitVerify(code));
+            assertFalse(EventStatusEnum.canStartDispose(code));
+        }
+    }
+
+    @Test
+    void optionsAndActiveFiltersFollowLifecycleOrder() {
+        assertEquals(Arrays.asList(0, 1, 2, 3, 4, 5), EventStatusEnum.codes());
+        assertEquals(Arrays.asList(0, 1, 2, 3), EventStatusEnum.activeCodes());
+        assertTrue(EventStatusEnum.canSubmitVerify(2));
+        assertFalse(EventStatusEnum.canAudit(2));
+    }
+}

+ 119 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/EventVerifyWithoutDepartmentTest.java

@@ -0,0 +1,119 @@
+package com.zksy.dispatch;
+
+import com.zksy.common.exception.ServiceException;
+import com.zksy.dispatch.domain.DisEvent;
+import com.zksy.dispatch.domain.DisEventLog;
+import com.zksy.dispatch.dto.DisUserOptionVO;
+import com.zksy.dispatch.dto.EventVerifyDTO;
+import com.zksy.dispatch.mapper.DisEventMapper;
+import com.zksy.dispatch.mapper.DispatchUserMapper;
+import com.zksy.dispatch.service.DisEventLogService;
+import com.zksy.dispatch.service.impl.DisEventServiceImpl;
+import com.zksy.system.service.ISysMenuService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class EventVerifyWithoutDepartmentTest {
+    private final DisEventMapper eventMapper = mock(DisEventMapper.class);
+    private final DispatchUserMapper userMapper = mock(DispatchUserMapper.class);
+    private final DisEventLogService logService = mock(DisEventLogService.class);
+    private final ISysMenuService sysMenuService = mock(ISysMenuService.class);
+    private final DisEventServiceImpl service = new DisEventServiceImpl();
+    private DisEvent event;
+    private EventVerifyDTO request;
+
+    @BeforeEach
+    void prepare() {
+        ReflectionTestUtils.setField(service, "baseMapper", eventMapper);
+        ReflectionTestUtils.setField(service, "userMapper", userMapper);
+        ReflectionTestUtils.setField(service, "eventLogService", logService);
+        ReflectionTestUtils.setField(service, "sysMenuService", sysMenuService);
+        event = new DisEvent();
+        event.setId(1L);
+        event.setEventNo("SJ-TEST");
+        event.setStatus(0);
+        when(eventMapper.selectForLifecycleUpdate(1L)).thenReturn(event);
+
+        DisUserOptionVO handler = new DisUserOptionVO();
+        handler.setUserId(10L);
+        handler.setNickName("处置人");
+        // 无所属部门的有效用户也可直接选择。
+        DisUserOptionVO reviewer = new DisUserOptionVO();
+        reviewer.setUserId(20L);
+        reviewer.setNickName("核验人");
+        when(userMapper.selectUserOptions()).thenReturn(Arrays.asList(handler, reviewer));
+        when(sysMenuService.selectMenuPermsByUserId(10L)).thenReturn(Set.of("dispatch:event:handle"));
+        when(sysMenuService.selectMenuPermsByUserId(20L)).thenReturn(Set.of("dispatch:event:audit"));
+
+        request = new EventVerifyDTO();
+        request.setVerifyResult(0);
+        request.setEventCategory("1");
+        request.setEventLevel("2");
+        request.setHandlerId(10L);
+        request.setVerifierId(20L);
+    }
+
+    @Test
+    void validHandlerWithoutDepartmentCanBeAssignedAndLogged() {
+        service.verify(1L, request);
+
+        assertEquals(1, event.getStatus());
+        assertEquals(10L, event.getHandlerId());
+        assertEquals("处置人", event.getHandler());
+        assertEquals(20L, event.getVerifierId());
+        verify(userMapper, never()).selectDeptOptions();
+        verify(eventMapper).updateById(event);
+        ArgumentCaptor<DisEventLog> log = ArgumentCaptor.forClass(DisEventLog.class);
+        verify(logService).append(log.capture());
+        assertEquals(0, log.getValue().getStatusFrom());
+        assertEquals(1, log.getValue().getStatusTo());
+    }
+
+    @Test
+    void removingDepartmentRequirementStillRejectsInvalidHandler() {
+        request.setHandlerId(999L);
+
+        ServiceException error = assertThrows(ServiceException.class, () -> service.verify(1L, request));
+
+        assertEquals("处置人必须从有效系统用户中选择", error.getMessage());
+        verify(eventMapper, never()).updateById(any(DisEvent.class));
+        verifyNoInteractions(logService);
+    }
+
+    @Test
+    void candidateListsAreSeparatedByOperationPermission() {
+        assertEquals(10L, service.listHandlerOptions().get(0).getUserId());
+        assertEquals(20L, service.listVerifierOptions().get(0).getUserId());
+    }
+
+    @Test
+    void handlerWithoutHandlePermissionCannotBeAssigned() {
+        when(sysMenuService.selectMenuPermsByUserId(10L)).thenReturn(Collections.emptySet());
+
+        ServiceException error = assertThrows(ServiceException.class, () -> service.verify(1L, request));
+
+        assertEquals("处置人未配置对应事件操作权限", error.getMessage());
+        verify(eventMapper, never()).updateById(any(DisEvent.class));
+        verifyNoInteractions(logService);
+    }
+
+    @Test
+    void verifierWithoutAuditPermissionCannotBeAssigned() {
+        when(sysMenuService.selectMenuPermsByUserId(20L)).thenReturn(Collections.emptySet());
+
+        ServiceException error = assertThrows(ServiceException.class, () -> service.verify(1L, request));
+
+        assertEquals("核验人未配置对应事件操作权限", error.getMessage());
+        verify(eventMapper, never()).updateById(any(DisEvent.class));
+        verifyNoInteractions(logService);
+    }
+}

+ 31 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/support/DispatchAttachUrlSupportTest.java

@@ -0,0 +1,31 @@
+package com.zksy.dispatch.support;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DispatchAttachUrlSupportTest {
+    private final DispatchAttachUrlSupport support = new DispatchAttachUrlSupport();
+
+    @BeforeEach
+    void setUp() {
+        ReflectionTestUtils.setField(support, "endpoint", "https://minio.example.com/");
+        ReflectionTestUtils.setField(support, "bucket", "dispatch");
+    }
+
+    @Test
+    void convertsEveryCommaSeparatedAttachmentToAccessibleUrl() {
+        assertEquals(
+                "https://minio.example.com/dispatch/2026/a.jpg,https://minio.example.com/dispatch/2026/b.png",
+                support.toAccessibleUrl("/2026/a.jpg, /2026/b.png"));
+    }
+
+    @Test
+    void preservesExternalUrlsWhileConvertingRelativeAttachments() {
+        assertEquals(
+                "/2026/a.jpg,https://cdn.example.com/b.png",
+                support.toRelative("https://minio.example.com/dispatch/2026/a.jpg,https://cdn.example.com/b.png"));
+    }
+}