Просмотр исходного кода

移动端-应急-事件管理,预案管理

zengy 14 часов назад
Родитель
Сommit
e72f88db31
36 измененных файлов с 834 добавлено и 325 удалено
  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. 160 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/mobile/MobileEmergencyController.java
  6. 17 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/system/SysLoginController.java
  7. 5 23
      pipe-network-service/zksy-framework/src/main/java/com/zksy/framework/web/service/SysPermissionService.java
  8. 1 1
      pipe-network-service/zksy-framework/src/main/java/com/zksy/framework/web/service/UserDetailsServiceImpl.java
  9. 3 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEvent.java
  10. 9 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEventLog.java
  11. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisPlan.java
  12. 3 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisDeptOptionVO.java
  13. 3 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventPageDTO.java
  14. 18 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventVO.java
  15. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisHomeVO.java
  16. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisUserOptionVO.java
  17. 23 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventAuditDTO.java
  18. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventReleaseDTO.java
  19. 0 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventUrgeRowVO.java
  20. 2 8
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventVerifyDTO.java
  21. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanApproveDTO.java
  22. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanPageDTO.java
  23. 8 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/enums/EventLogOperTypeEnum.java
  24. 13 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/enums/EventStatusEnum.java
  25. 2 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DispatchUserMapper.java
  26. 15 14
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventService.java
  27. 127 55
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventServiceImpl.java
  28. 16 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisPlanServiceImpl.java
  29. 23 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/support/DispatchAttachUrlSupport.java
  30. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/mapper/SysUserMapper.java
  31. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/ISysUserService.java
  32. 91 142
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/impl/SysUserServiceImpl.java
  33. 6 0
      pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml
  34. 46 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/EventStatusContractTest.java
  35. 119 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/dispatch/EventVerifyWithoutDepartmentTest.java
  36. 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("已驳回");
     }

+ 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);
         // 权限集合

+ 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);

+ 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;
         }

+ 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"));
+    }
+}