Selaa lähdekoodia

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/foundation/CockpitFreshnessService.java
Kazerin 1 kuukausi sitten
vanhempi
commit
6687518763
49 muutettua tiedostoa jossa 2532 lisäystä ja 784 poistoa
  1. 140 0
      .feisuan/rules/project_rule.md
  2. 6 0
      package-lock.json
  3. 36 0
      pipe-network-service/sql/migration_gas_workorder_menu.sql
  4. 41 0
      pipe-network-service/sql/migration_work_order_level.sql
  5. 134 0
      pipe-network-service/sql/migration_work_order_status_refactor.sql
  6. 10 7
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalController.java
  7. 10 6
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceMaintain/DeviceMaintainController.java
  8. 19 9
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java
  9. 6 4
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageWarningController.java
  10. 147 136
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/BusinessWorkOrderController.java
  11. 183 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/ops/PworkOrderController.java
  12. 9 9
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalControllerTest.java
  13. 1 1
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitEquipmentDetailAggregateControllerTest.java
  14. 1 2
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitFreshnessSemanticsControllerTest.java
  15. 0 2
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitWorkOrderReadControllerTest.java
  16. 7 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java
  17. 12 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/AlarmData.java
  18. 35 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmDataServiceImpl.java
  19. 19 122
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/WorkOrder.java
  20. 24 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/WorkOrderLog.java
  21. 6 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/WorkOrderPushVO.java
  22. 4 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillAlarmSyncService.java
  23. 52 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderDelayApplyStatusEnum.java
  24. 60 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderLevelEnum.java
  25. 68 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderLogOperTypeEnum.java
  26. 146 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderStatusEnum.java
  27. 63 43
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/BusinessWorkOrderService.java
  28. 5 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/DrillAlarmSyncService.java
  29. 589 339
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImpl.java
  30. 13 17
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImpl.java
  31. 77 40
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WorkOrderServiceImpl.java
  32. 0 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/BusinessCockpitWorkOrderReadAdapter.java
  33. 0 8
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderSummary.java
  34. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/DrainageWarning.java
  35. 4 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageWarningService.java
  36. 78 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageWarningServiceImpl.java
  37. 38 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDelayApplyDTO.java
  38. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDelayAuditDTO.java
  39. 29 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDetailVO.java
  40. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDispatchDTO.java
  41. 33 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderOperateDTO.java
  42. 71 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderPageDTO.java
  43. 38 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderVerifyDTO.java
  44. 195 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/support/OpsAssignScopeSupport.java
  45. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/mapper/SysUserMapper.java
  46. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/ISysUserService.java
  47. 16 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/impl/SysUserServiceImpl.java
  48. 9 7
      pipe-network-service/zksy-system/src/main/resources/mapper/WorkOrderMapper.xml
  49. 10 0
      pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml

+ 140 - 0
.feisuan/rules/project_rule.md

@@ -0,0 +1,140 @@
+
+# 开发规范指南
+为保证代码质量、可维护性、安全性与可扩展性,请在开发过程中严格遵循以下规范。
+
+## 一、项目基本信息
+
+- **工作目录**:`E:\work\pipe-ner-server`
+- **代码作者**:Administrator
+- **构建工具**:Maven
+- **JDK 版本**:Java 11 (JDK 11.0.27)
+- **核心框架**:Spring Boot 2.7.12
+- **微服务架构**:Spring Cloud 2021.0.3 + Spring Cloud Alibaba 2021.0.4.0
+
+## 二、项目目录结构
+
+本项目采用多模块 Maven 架构,包含网关、公共模块及多个业务服务模块。
+
+```text
+pipe-ner-server
+├── audio-service                 # 音频服务
+├── docs                          # 文档
+├── environment-service           # 环境服务
+├── firefighting-pressure-service # 消防压力服务
+├── flammable-gas-service         # 易燃气体服务
+├── manhole-service               # 井盖服务
+├── pipe-network-service          # 管网核心服务
+│   ├── zksy-admin                # Web服务入口
+│   ├── zksy-common               # 通用工具
+│   ├── zksy-framework            # 框架核心
+│   ├── zksy-generator            # 代码生成
+│   ├── zksy-quartz               # 定时任务
+│   └── zksy-system               # 系统模块
+├── radar-service                 # 雷达服务
+├── telemetry-service             # 遥测服务
+├── water-level-service           # 水位服务
+├── zk-api-service                # API服务
+├── zk-common                     # 公共模块
+└── zk-gateway                    # 网关服务
+```
+
+## 三、技术栈要求
+
+- **主框架**:Spring Boot 2.7.12
+- **语言版本**:Java 11
+- **数据库**:KingbaseES (人大金仓) / MySQL
+- **ORM框架**:MyBatis-Plus 3.5.7
+- **核心依赖**:
+  - `spring-boot-starter-web`
+  - `mybatis-plus-boot-starter`
+  - `lombok`
+  - `hutool-all` (工具类库)
+  - `spring-cloud-starter-alibaba-nacos` (注册中心/配置中心)
+  - `spring-cloud-starter-alibaba-sentinel` (熔断限流)
+  - `knife4j-openapi2-spring-boot-starter` (API文档)
+
+## 四、分层架构规范
+
+| 层级        | 职责说明                         | 开发约束与注意事项                                               |
+|-------------|----------------------------------|----------------------------------------------------------------|
+| **Controller** | 处理 HTTP 请求与响应,定义 API 接口 | 使用 `@RestController`;不得包含业务逻辑,仅负责参数校验和结果封装 |
+| **Service**    | 实现业务逻辑、事务管理与数据校验   | 接口定义在 `service` 包,实现类放在 `service.impl` 包;事务注解 `@Transactional` 仅在此层使用 |
+| **Mapper**     | 数据库访问与持久化操作             | 继承 `BaseMapper`;对应 XML 文件需放在 `resources/mapper` 目录下 |
+| **Domain/Entity** | 映射数据库表结构及数据传输对象     | 实体类建议放在 `domain` 包;DTO/VO/Query 对象按职责定义          |
+
+### 接口与实现分离
+
+- 所有 Service 接口实现类需放在接口所在包下的 `impl` 子包中。
+
+## 五、安全与性能规范
+
+### 输入校验
+
+- 使用 `@Valid` 与 JSR-303 校验注解(如 `@NotBlank`, `@Size` 等)
+  - **注意**:Spring Boot 2.x 中校验注解位于 `javax.validation.constraints.*`
+- 禁止在 XML 文件中使用 `${}` 拼接 SQL,防止 SQL 注入,必须使用 `#{}`。
+
+### 事务管理
+
+- `@Transactional` 注解仅用于 **Service 层**方法。
+- 避免在循环中频繁提交事务,影响性能。
+
+## 六、代码风格规范
+
+### 命名规范
+
+| 类型       | 命名方式             | 示例                  |
+|------------|----------------------|-----------------------|
+| 类名       | UpperCamelCase       | `UserServiceImpl`     |
+| 方法/变量  | lowerCamelCase       | `saveUser()`          |
+| 常量       | UPPER_SNAKE_CASE     | `MAX_LOGIN_ATTEMPTS`  |
+
+### 注释规范
+
+- 所有类、接口、公共方法需添加 **Javadoc** 注释。
+- **注释语言**:必须使用中文(用户第一语言)。
+- 作者信息统一填写:`@author Administrator`
+
+### 类型命名规范(阿里巴巴风格)
+
+| 后缀 | 用途说明                     | 示例         |
+|------|------------------------------|--------------|
+| DTO  | 数据传输对象                 | `UserDTO`    |
+| Entity/DO   | 数据库实体对象               | `UserDO`     |
+| BO   | 业务逻辑封装对象             | `UserBO`     |
+| VO   | 视图展示对象                 | `UserVO`     |
+| Query| 查询参数封装对象             | `UserQuery`  |
+
+### 实体类简化工具
+
+- 使用 Lombok 注解替代手动编写 getter/setter/构造方法:
+  - `@Data`
+  - `@NoArgsConstructor`
+  - `@AllArgsConstructor`
+  - `@Builder` (如需链式调用)
+
+## 七、扩展性与日志规范
+
+### 接口优先原则
+
+- 所有业务逻辑通过接口定义(如 `UserService`),具体实现放在 `impl` 包中(如 `UserServiceImpl`)。
+
+### 日志记录
+
+- 使用 `@Slf4j` 注解代替 `System.out.println`。
+- 日志级别规范:开发环境使用 DEBUG,生产环境使用 INFO 或 WARN。
+
+### 工具类使用
+
+- 优先使用 `hutool-all` 工具包中的工具类(如 `StrUtil`, `CollUtil` 等),避免重复造轮子。
+- 日期处理优先使用 `java.time` 包下的类。
+
+## 八、编码原则总结
+
+| 原则       | 说明                                       |
+|------------|--------------------------------------------|
+| **SOLID**  | 高内聚、低耦合,增强可维护性与可扩展性     |
+| **DRY**    | 避免重复代码,提高复用性                   |
+| **KISS**   | 保持代码简洁易懂                           |
+| **YAGNI**  | 不实现当前不需要的功能                     |
+| **OWASP**  | 防范常见安全漏洞,如 SQL 注入、XSS 等      |

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "pipe-ner-server",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}

+ 36 - 0
pipe-network-service/sql/migration_gas_workorder_menu.sql

@@ -0,0 +1,36 @@
+-- =====================================================================
+-- 燃气「工单管理」菜单复用运维工单处置页面
+--
+-- 背景:原「燃气工单 - 工单管理」菜单对应独立的
+--       subSystem/basic/BusinessWorkOrder.vue(界面与运维工单处置重复),
+--       现统一复用 subSystem/pipeNetwork/ops/workOrder/dispose.vue。
+--
+-- 业务口径:燃气菜单只查询「燃气类设备」产生的工单,过滤链路为
+--       work_order.device_id      -> equipment_base.equipment_id
+--       equipment_base.equipment_type_id -> equipment_type.id
+--       equipment_type.parent_type_id    = '3'
+--   该过滤由前端固定传 deviceTypeParentId=3、后端 WorkOrderPageDTO /
+--   BusinessWorkOrderServiceImpl#applyDeviceTypeScope 实现;运维菜单不传该参数,仍查全量。
+--
+-- 执行前请备份数据!
+-- =====================================================================
+
+-- 1. 菜单指向新的复用组件(按原 component 定位,不依赖具体 menu_id / path)
+UPDATE sys_menu
+   SET component = 'subSystem/pipeNetwork/ops/workOrder/dispose'
+ WHERE component = 'subSystem/basic/BusinessWorkOrder';
+
+-- =====================================================================
+-- 2. 校验:确认已被更新,且运维侧两个菜单仍指向对应组件
+--    SELECT menu_id, menu_name, path, component FROM sys_menu
+--     WHERE component IN ('subSystem/pipeNetwork/ops/workOrder/dispose',
+--                         'subSystem/pipeNetwork/ops/workOrder/index',
+--                         'subSystem/pipeNetwork/pworkOrder/Pworkdisposition');
+--
+-- 3. 说明
+--    - 不修改 path 与 perms:原菜单的访问权限标识保持不变,已分配的
+--      sys_role_menu 授权无需调整。
+--    - dispose.vue 依据路由 path 是否包含 'BusinessWorkOrder' 判定燃气模式,
+--      若此处菜单 path 非该值,需同步调整前端 GAS_MENU_PATHS 常量。
+--    - 旧的 BusinessWorkOrder.vue 已无菜单引用,确认无其他用处后可删除。
+-- =====================================================================

+ 41 - 0
pipe-network-service/sql/migration_work_order_level.sql

@@ -0,0 +1,41 @@
+-- =====================================================================
+-- 工单优先级(work_order.order_level)取值重定义迁移脚本
+--
+-- 旧:1-紧急 2-一般 3-低
+-- 新:0-紧急 1-一般 2-低
+--
+-- 配套代码改动:
+--   com.zksy.base.enums.WorkOrderLevelEnum  URGENT(0) NORMAL(1) LOW(2)
+--   前端 src/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js
+--     的 ORDER_LEVEL_MAP 同步改为 0/1/2
+--
+-- 注意:本脚本【仅可执行一次】!
+--   迁移后合法值为 0/1/2,其中 1、2 与旧值重合,
+--   重复执行会把「一般(1)」再次误改为「紧急(0)」。
+--   执行前请备份数据。
+-- =====================================================================
+
+-- 1) 更新字段注释
+COMMENT ON COLUMN app_user.work_order.order_level IS '工单优先级:0-紧急 1-一般 2-低';
+
+-- 2) 历史数据迁移(单条 UPDATE 原子完成,避免中间值被二次匹配)
+UPDATE app_user.work_order
+   SET order_level = CASE order_level
+                         WHEN 1 THEN 0   -- 紧急
+                         WHEN 2 THEN 1   -- 一般
+                         WHEN 3 THEN 2   -- 低
+                         ELSE order_level
+                     END
+ WHERE order_level IN (1, 2, 3);
+
+-- 3) 兜底:非法/空值统一置为「一般(1)」
+UPDATE app_user.work_order
+   SET order_level = 1
+ WHERE order_level IS NULL
+    OR order_level NOT IN (0, 1, 2);
+
+-- =====================================================================
+-- 快速核验:
+--   SELECT order_level, count(*) FROM app_user.work_order GROUP BY order_level ORDER BY order_level;
+--   期望只出现 0 / 1 / 2 三种取值。
+-- =====================================================================

+ 134 - 0
pipe-network-service/sql/migration_work_order_status_refactor.sql

@@ -0,0 +1,134 @@
+-- =====================================================================
+-- 工单流程重构迁移脚本
+-- 1) work_order:新增 fault_time / delay_apply_status,废弃 order_type,
+--    receive_user 改为单个责任人(BIGINT),状态值重定义为 0~7
+-- 2) work_order_log:新增 attach_url,oper_type 重定义为 0~10
+-- 3) 新增工单按钮权限(父菜单 menu_id = 2082)
+-- 执行前请备份数据!
+-- =====================================================================
+
+-- ---------------------------- 1. work_order 结构变更 ----------------------------
+ALTER TABLE app_user.work_order ADD COLUMN IF NOT EXISTS fault_time TIMESTAMP;
+ALTER TABLE app_user.work_order ADD COLUMN IF NOT EXISTS delay_apply_status SMALLINT DEFAULT 0;
+ALTER TABLE app_user.work_order DROP COLUMN IF EXISTS order_type;
+
+-- receive_user:原 VARCHAR(多人逗号分隔) → BIGINT(单个责任人)
+ALTER TABLE app_user.work_order ALTER COLUMN receive_user TYPE BIGINT USING NULLIF(receive_user, '')::BIGINT;
+
+COMMENT ON COLUMN app_user.work_order.fault_time IS '故障发生时间';
+COMMENT ON COLUMN app_user.work_order.delay_apply_status IS '延期申请状态:0-无延期申请 1-待延期审核 2-延期审核通过 3-延期审核驳回';
+COMMENT ON COLUMN app_user.work_order.receive_user IS '责任人用户ID';
+COMMENT ON COLUMN app_user.work_order.order_status IS '工单状态:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消';
+COMMENT ON COLUMN app_user.work_order.order_level IS '工单优先级:0-紧急 1-一般 2-低';
+
+-- ---------------------------- 2. 历史状态数据迁移 ----------------------------
+-- 旧:1-待派单 2-待接单 3-已接单 4-处理中 5-待验收 6-已结案 7-已驳回 8-延期审核 9-已延期
+-- 新:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消
+UPDATE app_user.work_order SET order_status = 0 WHERE order_status = 1;
+UPDATE app_user.work_order SET order_status = 1 WHERE order_status = 2;
+UPDATE app_user.work_order SET order_status = 2 WHERE order_status = 3;
+UPDATE app_user.work_order SET order_status = 3 WHERE order_status = 4;
+UPDATE app_user.work_order SET order_status = 4 WHERE order_status = 5;
+UPDATE app_user.work_order SET order_status = 5 WHERE order_status = 6;
+-- 旧 7-已驳回 → 7-已取消(值相同,无需迁移)
+-- 旧 8-延期审核 → 处理中 + 待延期审核;旧 9-已延期 → 延期中 + 延期审核通过
+UPDATE app_user.work_order
+   SET order_status = 3,
+       delay_apply_status = 1
+ WHERE order_status = 8;
+UPDATE app_user.work_order
+   SET order_status = 6,
+       delay_apply_status = 2
+ WHERE order_status = 9;
+
+UPDATE app_user.work_order SET fault_time = COALESCE(fault_time, create_time);
+UPDATE app_user.work_order SET delay_apply_status = COALESCE(delay_apply_status, 0);
+
+-- ---------------------------- 3. work_order_log 结构变更 ----------------------------
+ALTER TABLE app_user.work_order_log ADD COLUMN IF NOT EXISTS attach_url VARCHAR(2000);
+COMMENT ON COLUMN app_user.work_order_log.attach_url IS '操作附件URL,多个使用逗号分隔';
+COMMENT ON COLUMN app_user.work_order_log.oper_type IS '操作类型:0-创建工单 1-派单 2-接单 3-处置 4-提交验收 5-验收通过 6-验收驳回 7-取消工单 8-申请延期 9-延期审核通过 10-延期审核驳回';
+
+-- 旧:1-创建工单 2-派单 3-接单 4-开始维修 5-提交验收 6-验收通过 7-驳回工单 8-申请延期 9-延期审核通过
+-- 新:0-创建工单 1-派单 2-接单 3-处置 4-提交验收 5-验收通过 6-验收驳回 7-取消工单 8-申请延期 9-延期审核通过 10-延期审核驳回
+UPDATE app_user.work_order_log SET oper_type = 0 WHERE oper_type = 1;
+UPDATE app_user.work_order_log SET oper_type = 1 WHERE oper_type = 2;
+UPDATE app_user.work_order_log SET oper_type = 2 WHERE oper_type = 3;
+UPDATE app_user.work_order_log SET oper_type = 3 WHERE oper_type = 4;
+UPDATE app_user.work_order_log SET oper_type = 4 WHERE oper_type = 5;
+UPDATE app_user.work_order_log SET oper_type = 5 WHERE oper_type = 6;
+UPDATE app_user.work_order_log SET oper_type = 7 WHERE oper_type = 7;
+-- 旧 7-驳回工单、新 7-取消工单 值相同,如需区分可按 oper_desc 人工判断
+UPDATE app_user.work_order_log SET oper_type = 8 WHERE oper_type = 8;
+UPDATE app_user.work_order_log SET oper_type = 9 WHERE oper_type = 9;
+
+CREATE INDEX IF NOT EXISTS idx_work_order_status ON app_user.work_order (order_status);
+CREATE INDEX IF NOT EXISTS idx_work_order_receive_user ON app_user.work_order (receive_user);
+CREATE INDEX IF NOT EXISTS idx_work_order_log_order_id ON app_user.work_order_log (order_id);
+
+-- =====================================================================
+-- 4. 工单菜单及按钮权限(父菜单 menu_id = 2082)
+-- =====================================================================
+-- 主菜单:故障工单
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '故障工单', 2082, 1, 'PworkList', 'subSystem/pipeNetwork/ops/workOrder/index', 1, 0, 'C',
+       '0', '0', 'ops:pworkOrder:query', 'workorder', 'admin', CURRENT_TIMESTAMP, '故障工单列表菜单'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id = 2082 AND path = 'PworkList');
+
+-- 工单处置页面(隐藏菜单,承载全部操作按钮)
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '工单处置', 2082, 2, 'Pworkdisposition', 'subSystem/pipeNetwork/ops/workOrder/dispose', 1, 0, 'C',
+       '1', '0', 'ops:pworkOrder:query', 'edit', 'admin', CURRENT_TIMESTAMP, '工单处置页面'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id = 2082 AND path = 'Pworkdisposition');
+
+-- 前端页面文件迁移:subSystem/pipeNetwork/pworkOrder/* → subSystem/pipeNetwork/ops/workOrder/*
+--    PworkList.vue -> index.vue,Pworkdisposition.vue -> dispose.vue
+-- 对已执行过旧脚本(component 仍为旧路径)的环境,用下面的 UPDATE 幂等修正;新环境 INSERT 已写入新路径。
+UPDATE sys_menu SET component = 'subSystem/pipeNetwork/ops/workOrder/index'
+ WHERE component = 'subSystem/pipeNetwork/pworkOrder/PworkList';
+
+UPDATE sys_menu SET component = 'subSystem/pipeNetwork/ops/workOrder/dispose'
+ WHERE component = 'subSystem/pipeNetwork/pworkOrder/Pworkdisposition';
+
+-- 按钮:新增故障工单
+INSERT INTO sys_menu (menu_name, parent_id, order_num, is_frame, is_cache, menu_type, visible, status,
+                      perms, create_by, create_time, remark)
+SELECT '新增故障工单', m.menu_id, 1, 1, 0, 'F', '0', '0', 'ops:pworkOrder:add', 'admin', CURRENT_TIMESTAMP, '新增故障工单按钮'
+  FROM sys_menu m WHERE m.parent_id = 2082 AND m.path = 'PworkList'
+   AND NOT EXISTS (SELECT 1 FROM sys_menu b WHERE b.perms = 'ops:pworkOrder:add');
+
+-- 按钮:派单 / 接单 / 处置 / 提交验收 / 验收 / 取消 / 申请延期 / 延期审核 / 删除
+INSERT INTO sys_menu (menu_name, parent_id, order_num, is_frame, is_cache, menu_type, visible, status,
+                      perms, create_by, create_time, remark)
+SELECT t.name, m.menu_id, t.ord, 1, 0, 'F', '0', '0', t.perms, 'admin', CURRENT_TIMESTAMP, t.name
+  FROM sys_menu m
+  JOIN (VALUES ('派单', 2, 'ops:pworkOrder:dispatch'),
+               ('核查接单', 3, 'ops:pworkOrder:accept'),
+               ('处置', 4, 'ops:pworkOrder:process'),
+               ('提交验收', 5, 'ops:pworkOrder:submitVerify'),
+               ('验收', 6, 'ops:pworkOrder:verify'),
+               ('取消工单', 7, 'ops:pworkOrder:cancel'),
+               ('申请延期', 8, 'ops:pworkOrder:delay'),
+               ('延期审核', 9, 'ops:pworkOrder:delayAudit'),
+               ('删除工单', 10, 'ops:pworkOrder:remove')) AS t(name, ord, perms)
+    ON m.parent_id = 2082 AND m.path = 'Pworkdisposition'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu b WHERE b.perms = t.perms);
+
+-- =====================================================================
+-- 5. 将「故障工单」菜单及按钮权限授予所有现有角色
+--    说明:改造前历史接口 /api/work-order 无权限校验,所有登录用户均可操作工单;
+--          本次为该接口补齐 ops:pworkOrder:* 权限后,为避免现有角色功能不可用,
+--          默认将 2082 下全部菜单与按钮授予所有角色(等价改造前的可用性)。
+--          如需精细化授权,可在「系统管理 - 角色管理」中收回,或按需单独分配。
+-- =====================================================================
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT r.role_id, m.menu_id
+  FROM sys_role r
+  JOIN sys_menu m
+    ON (m.parent_id = 2082 AND m.path IN ('PworkList', 'Pworkdisposition'))
+    OR (m.menu_type = 'F' AND m.perms LIKE 'ops:pworkOrder:%')
+  LEFT JOIN sys_role_menu rm
+    ON rm.role_id = r.role_id AND rm.menu_id = m.menu_id
+ WHERE rm.menu_id IS NULL;

+ 10 - 7
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalController.java

@@ -13,6 +13,8 @@ import com.zksy.common.enums.BusinessType;
 import com.zksy.common.utils.poi.ExcelUtil;
 import com.zksy.common.utils.SecurityUtils;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.base.enums.WorkOrderStatusEnum;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -75,18 +77,19 @@ public class DeviceAbnormalController {
                 return AjaxResult.error("派单失败:工单创建未返回有效工单ID");
             }
             if (receiveUserId == null) {
-                order.setOrderStatus(1);
+                order.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
                 return AjaxResult.success("工单创建成功,待派单", order);
             }
-            Long operatorId = SecurityUtils.getUserId();
-            String operatorName = SecurityUtils.getUsername();
-            boolean dispatched = businessWorkOrderService.dispatchOrder(order.getOrderId(), receiveUserId,
-                    vo.getMaintainRemark(), operatorId, operatorName);
+            WorkOrderDispatchDTO dispatchDTO = new WorkOrderDispatchDTO();
+            dispatchDTO.setOrderId(order.getOrderId());
+            dispatchDTO.setHandlerId(receiveUserId);
+            dispatchDTO.setRemarks(vo.getMaintainRemark());
+            boolean dispatched = businessWorkOrderService.dispatch(dispatchDTO);
             if (!dispatched) {
                 return AjaxResult.error("派单失败:工单仍为待派单状态,请稍后重试");
             }
-            order.setReceiveUser(String.valueOf(receiveUserId));
-            order.setOrderStatus(2);
+            order.setReceiveUser(receiveUserId);
+            order.setOrderStatus(WorkOrderStatusEnum.PENDING_ACCEPT.getCode());
             return AjaxResult.success("派单成功", order);
         } catch (ServiceException e) {
             log.warn("供水异常设备派单业务失败: {}", e.getMessage());

+ 10 - 6
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceMaintain/DeviceMaintainController.java

@@ -11,6 +11,8 @@ import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
 import com.zksy.common.utils.SecurityUtils;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.base.enums.WorkOrderStatusEnum;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -46,17 +48,19 @@ public class DeviceMaintainController {
                 return AjaxResult.error("派单失败:工单创建未返回有效工单ID");
             }
             if (receiveUserId == null) {
-                order.setOrderStatus(1);
+                order.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
                 return AjaxResult.success("工单创建成功,待派单", order);
             }
-            boolean dispatched = businessWorkOrderService.dispatchOrder(order.getOrderId(),
-                    receiveUserId, vo.getMaintainRemark(), SecurityUtils.getUserId(),
-                    SecurityUtils.getUsername());
+            WorkOrderDispatchDTO dispatchDTO = new WorkOrderDispatchDTO();
+            dispatchDTO.setOrderId(order.getOrderId());
+            dispatchDTO.setHandlerId(receiveUserId);
+            dispatchDTO.setRemarks(vo.getMaintainRemark());
+            boolean dispatched = businessWorkOrderService.dispatch(dispatchDTO);
             if (!dispatched) {
                 return AjaxResult.error("派单失败:工单仍为待派单状态,请稍后重试");
             }
-            order.setReceiveUser(String.valueOf(receiveUserId));
-            order.setOrderStatus(2);
+            order.setReceiveUser(receiveUserId);
+            order.setOrderStatus(WorkOrderStatusEnum.PENDING_ACCEPT.getCode());
             return AjaxResult.success("派单成功", order);
         } catch (ServiceException e) {
             log.warn("创建维修派单业务失败: {}", e.getMessage());

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

@@ -62,11 +62,22 @@ public class AlarmController {
             return AjaxResult.error("报警信息不存在");
         }
 
-        long existing = businessWorkOrderService.lambdaQuery()
+        WorkOrder existingOrder = businessWorkOrderService.lambdaQuery()
                 .eq(WorkOrder::getAlarmId, alarmId)
-                .count();
-        if (existing > 0) {
-            return AjaxResult.error("该报警已上传工单,不能重复创建");
+                .orderByDesc(WorkOrder::getOrderId)
+                .last("LIMIT 1")
+                .one();
+        if (existingOrder != null) {
+            // 历史数据可能已经建单但未同步 alarm_status,审核接口按幂等成功处理。
+            if (!Integer.valueOf(1).equals(alarm.getAlarmStatus())) {
+                alarm.setAlarmStatus(1);
+                alarm.setHandleTime(LocalDateTime.now());
+                alarm.setHandleUser(SecurityUtils.getUsername());
+                alarm.setHandleRemark("报警已审核,关联工单已存在:" + existingOrder.getOrderId());
+                alarm.setUpdateTime(LocalDateTime.now());
+                service.updateById(alarm);
+            }
+            return AjaxResult.success("该报警已审核,关联工单已存在", existingOrder.getOrderId());
         }
 
         EquipmentBase equipment = alarm.getDeviceCode() == null ? null
@@ -77,13 +88,12 @@ public class AlarmController {
         order.setAlarmId(alarmId);
         order.setDeviceId(equipment != null ? equipment.getEquipmentId() : alarm.getDeviceCode());
         order.setDeviceCode(alarm.getDeviceCode());
-        order.setOrderType(1);
         Object orderLevel = body.get("orderLevel");
         order.setOrderLevel(orderLevel == null ? mapAlarmLevel(alarm.getAlarmLevel()) : Integer.valueOf(orderLevel.toString()));
         Object orderDesc = body.get("orderDesc");
         order.setOrderDesc(orderDesc == null || orderDesc.toString().trim().isEmpty()
                 ? buildOrderDesc(alarm) : orderDesc.toString());
-        Long orderId = businessWorkOrderService.createWorkOrder(order);
+        Long orderId = businessWorkOrderService.addWorkOrder(order);
 
         alarm.setAlarmStatus(1);
         alarm.setHandleTime(LocalDateTime.now());
@@ -95,9 +105,9 @@ public class AlarmController {
     }
 
     private Integer mapAlarmLevel(Integer alarmLevel) {
-        if (alarmLevel == null || alarmLevel == 1) return 1;
-        if (alarmLevel == 4) return 3;
-        return 2;
+        if (alarmLevel == null || alarmLevel == 1) return 0;
+        if (alarmLevel == 4) return 2;
+        return 1;
     }
 
     private String buildOrderDesc(AlarmData alarm) {

+ 6 - 4
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageWarningController.java

@@ -60,8 +60,9 @@ public class DrainageWarningController {
             @ApiParam("是否只返回当前预警") @RequestParam(defaultValue = "false") boolean currentOnly,
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String startTime,
-            @ApiParam("结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String endTime) {
-        return service.findVisibleCurrentPage(pageNum, pageSize, userId, deptId, isAdmin, warningLevel, warningType, status, currentOnly, equipmentName, startTime, endTime);
+            @ApiParam("结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String endTime,
+            @ApiParam("设备模块类型(drainage=排水)") @RequestParam(required = false) String equipmentType) {
+        return service.findVisibleCurrentPage(pageNum, pageSize, userId, deptId, isAdmin, warningLevel, warningType, status, currentOnly, equipmentName, startTime, endTime, equipmentType);
     }
 
     @Anonymous
@@ -76,8 +77,9 @@ public class DrainageWarningController {
             @ApiParam("开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String startTime,
             @ApiParam("结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String endTime,
             @ApiParam("是否只返回历史预警") @RequestParam(defaultValue = "false") boolean historyOnly,
-            @ApiParam("设备名称") @RequestParam(required = false) String equipmentName) {
-        return service.findHistoryPage(pageNum, pageSize, warningLevel, warningType, status, startTime, endTime, historyOnly, equipmentName);
+            @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
+            @ApiParam("设备模块类型(drainage=排水)") @RequestParam(required = false) String equipmentType) {
+        return service.findHistoryPage(pageNum, pageSize, warningLevel, warningType, status, startTime, endTime, historyOnly, equipmentName, equipmentType);
     }
 
     @Anonymous

+ 147 - 136
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/BusinessWorkOrderController.java

@@ -1,199 +1,210 @@
 package com.zksy.web.controller.gasbasic;
 
-import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.zksy.base.domain.WorkOrder;
-import com.zksy.base.domain.WorkOrderLog;
 import com.zksy.base.service.BusinessWorkOrderService;
-import com.zksy.common.core.controller.BaseController;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.utils.DateUtils;
-import com.zksy.common.utils.SecurityUtils;
+import com.zksy.ops.dto.WorkOrderDelayApplyDTO;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
+import com.zksy.ops.dto.WorkOrderOperateDTO;
+import com.zksy.ops.dto.WorkOrderPageDTO;
+import com.zksy.ops.dto.WorkOrderVerifyDTO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.*;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Date;
+import java.util.Map;
 
 /**
- * 业务工单管理 Controller
+ * 业务工单管理 Controller(历史兼容层)
+ *
+ * <p>保留历史 URL,供排水/供水/井盖等旧页面调用;内部统一委托 {@link BusinessWorkOrderService},
+ * 与新工单流程(/api/ops/pworkOrder)共用同一套状态机,避免出现两套状态定义。</p>
+ *
+ * @author zksy
  */
 @Slf4j
 @RestController
 @RequestMapping("/api/work-order")
-@Api(tags = "业务工单管理")
-public class BusinessWorkOrderController extends BaseController {
+@Api(tags = "业务工单管理(历史兼容)")
+public class BusinessWorkOrderController {
 
     @Autowired
     private BusinessWorkOrderService businessWorkOrderService;
 
+    /**
+     * 历史接口:查询工单列表
+     *
+     * <p>orderType 与 equipmentType 已废弃,不再参与过滤</p>
+     */
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:query')")
     @GetMapping("/list")
-    @ApiOperation("查询工单列表(equipmentType:gas-燃气 drainage-排水,为空默认燃气)")
-    public AjaxResult list(Integer orderType, Integer orderLevel, String orderStatus, String deviceCode, String keyword, String equipmentType, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) {
-
-        IPage<WorkOrder> page = businessWorkOrderService.selectWorkOrderList(orderType, orderLevel, orderStatus, deviceCode, keyword, equipmentType, pageNum, pageSize);
-
-        Map<String, Object> data = new HashMap<>();
-        data.put("records", page.getRecords());
-        data.put("total", page.getTotal());
-        data.put("pageNum", page.getCurrent());
-        data.put("pageSize", page.getSize());
-        return AjaxResult.success(data);
+    @ApiOperation("查询工单列表(orderType/equipmentType 已废弃)")
+    public AjaxResult list(@RequestParam(required = false) Integer orderType,
+                           @RequestParam(required = false) Integer orderLevel,
+                           @RequestParam(required = false) String orderStatus,
+                           @RequestParam(required = false) String deviceCode,
+                           @RequestParam(required = false) String keyword,
+                           @RequestParam(required = false) String equipmentType,
+                           @RequestParam(defaultValue = "1") Integer pageNum,
+                           @RequestParam(defaultValue = "10") Integer pageSize) {
+        WorkOrderPageDTO dto = new WorkOrderPageDTO();
+        dto.setOrderLevel(orderLevel);
+        dto.setOrderStatus(orderStatus);
+        dto.setDeviceCode(deviceCode);
+        dto.setKeyword(keyword);
+        dto.setPageNum(pageNum == null ? 1L : pageNum.longValue());
+        dto.setPageSize(pageSize == null ? 10L : pageSize.longValue());
+        return AjaxResult.success(businessWorkOrderService.findByPage(dto));
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:add')")
     @PostMapping("/add")
-    @ApiOperation("手动新增工单(故障维修/日常巡检/设备保养)")
+    @ApiOperation("手动新增故障工单")
     public AjaxResult add(@RequestBody WorkOrder workOrder) {
-        if (workOrder.getOrderDesc() == null || workOrder.getOrderDesc().isEmpty()) {
-            return AjaxResult.error("工单描述不能为空");
-        }
-        Long orderId = businessWorkOrderService.createWorkOrder(workOrder);
-        return AjaxResult.success("工单创建成功", orderId);
+        return AjaxResult.success("工单创建成功", businessWorkOrderService.addWorkOrder(workOrder));
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:query')")
     @GetMapping("/{orderId}")
     @ApiOperation("查询工单详情(含操作日志)")
     public AjaxResult detail(@PathVariable Long orderId) {
-        WorkOrder order = businessWorkOrderService.getWorkOrderDetail(orderId);
-        List<WorkOrderLog> logs = businessWorkOrderService.getWorkOrderLogs(orderId);
-
-        Map<String, Object> data = new HashMap<>();
-        data.put("order", order);
-        data.put("logs", logs);
-        return AjaxResult.success(data);
+        return AjaxResult.success(businessWorkOrderService.getDetailById(orderId));
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:dispatch')")
     @PostMapping("/dispatch")
-    @ApiOperation("派单")
+    @ApiOperation("派单(兼容 handlerIds 多人入参,仅取第一位作为责任人)")
     public AjaxResult dispatch(@RequestBody Map<String, Object> body) {
-        if (body.get("orderId") == null) {
-            return AjaxResult.error("工单ID不能为空");
-        }
-        Long orderId = Long.valueOf(body.get("orderId").toString());
-        String remarks = body.get("remarks") != null ? body.get("remarks").toString() : "";
-        Long userId = SecurityUtils.getUserId();
-        String userName = SecurityUtils.getUsername();
-
-        // 支持多人派单:优先使用 handlerIds 数组,兼容旧版 handlerId
-        if (body.get("handlerIds") != null) {
-            // 使用 List<?> 兼容 Jackson 解析出的 Integer/Long/String 类型
-            List<?> handlerIdList = (List<?>) body.get("handlerIds");
-            if (handlerIdList.isEmpty()) {
-                return AjaxResult.error("接单人不能为空");
-            }
-            List<Long> handlerIds = new ArrayList<>();
-            for (Object item : handlerIdList) {
-                if (item != null) {
-                    handlerIds.add(Long.valueOf(item.toString()));
-                }
+        WorkOrderDispatchDTO dto = new WorkOrderDispatchDTO();
+        dto.setOrderId(toLong(body.get("orderId")));
+        dto.setHandlerId(toLong(body.get("handlerId")));
+        dto.setRemarks(toStringValue(body.get("remarks")));
+        if (dto.getHandlerId() == null && body.get("handlerIds") instanceof java.util.List) {
+            java.util.List<?> ids = (java.util.List<?>) body.get("handlerIds");
+            if (ids.isEmpty()) {
+                return AjaxResult.error("责任人不能为空");
             }
-            businessWorkOrderService.batchDispatchOrder(orderId, handlerIds, remarks, userId, userName);
-        } else if (body.get("handlerId") != null) {
-            Long handlerId = Long.valueOf(body.get("handlerId").toString());
-            businessWorkOrderService.dispatchOrder(orderId, handlerId, remarks, userId, userName);
-        } else {
-            return AjaxResult.error("接单人不能为空");
+            dto.setHandlerId(toLong(ids.get(0)));
         }
-        return AjaxResult.success("派单成功");
+        return businessWorkOrderService.dispatch(dto)
+                ? AjaxResult.success("派单成功") : AjaxResult.error("派单失败");
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:accept')")
     @PutMapping("/verify")
-    @ApiOperation("核查")
-    public AjaxResult verify(@RequestBody Map<String, Object> body) {
-        Long orderId = Long.valueOf(body.get("orderId").toString());
-        String handleResult = body.get("handleResult") != null ? body.get("handleResult").toString() : "";
-        String solution = body.get("solution") != null ? body.get("solution").toString() : "";
-        Long userId = SecurityUtils.getUserId();
-        String userName = SecurityUtils.getUsername();
+    @ApiOperation("核查接单(历史:核查)")
+    public AjaxResult verify(@RequestBody WorkOrderOperateDTO dto) {
+        return businessWorkOrderService.accept(dto)
+                ? AjaxResult.success("核查完成") : AjaxResult.error("核查失败");
+    }
 
-        businessWorkOrderService.verifyOrder(orderId, handleResult, solution, userId, userName);
-        return AjaxResult.success("核查完成");
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:accept')")
+    @PutMapping("/accept")
+    @ApiOperation("接单(历史接口)")
+    public AjaxResult accept(@RequestBody WorkOrderOperateDTO dto) {
+        return businessWorkOrderService.accept(dto)
+                ? AjaxResult.success("接单成功") : AjaxResult.error("接单失败");
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:process')")
     @PutMapping("/process")
-    @ApiOperation("处理")
-    public AjaxResult process(@RequestBody Map<String, Object> body) {
-        Long orderId = Long.valueOf(body.get("orderId").toString());
-        String handleResult = body.get("handleResult") != null ? body.get("handleResult").toString() : "";
-        String solution = body.get("solution") != null ? body.get("solution").toString() : "";
-        String maintenanceBeforeImg = body.get("maintenanceBeforeImg") != null ? body.get("maintenanceBeforeImg").toString() : "";
-        Long userId = SecurityUtils.getUserId();
-        String userName = SecurityUtils.getUsername();
+    @ApiOperation("工单处置")
+    public AjaxResult process(@RequestBody WorkOrderOperateDTO dto) {
+        return businessWorkOrderService.process(dto)
+                ? AjaxResult.success("处置成功") : AjaxResult.error("处置失败");
+    }
 
-        businessWorkOrderService.processOrder(orderId, handleResult, solution, maintenanceBeforeImg, userId, userName);
-        return AjaxResult.success("处理完成");
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:process')")
+    @PutMapping("/start")
+    @ApiOperation("开始处理(历史接口,等价处置)")
+    public AjaxResult start(@RequestBody WorkOrderOperateDTO dto) {
+        return businessWorkOrderService.process(dto)
+                ? AjaxResult.success("处理成功") : AjaxResult.error("处理失败");
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:submitVerify')")
     @PutMapping("/complete")
-    @ApiOperation("完成/提交验收")
-    public AjaxResult complete(@RequestBody Map<String, Object> body) {
-        Long orderId = Long.valueOf(body.get("orderId").toString());
-        String result = body.get("result") != null ? body.get("result").toString() : "";
-        String maintenanceAfterImg = body.get("maintenanceAfterImg") != null
-                ? body.get("maintenanceAfterImg").toString() : "";
-        Long userId = SecurityUtils.getUserId();
-        String userName = SecurityUtils.getUsername();
-
-        businessWorkOrderService.completeOrder(orderId, result, maintenanceAfterImg, userId, userName);
-        return AjaxResult.success("提交验收成功");
+    @ApiOperation("提交验收(历史:完成)")
+    public AjaxResult complete(@RequestBody WorkOrderOperateDTO dto) {
+        return businessWorkOrderService.submitVerify(dto)
+                ? AjaxResult.success("提交验收成功") : AjaxResult.error("提交验收失败");
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:verify')")
     @PutMapping("/close")
-    @ApiOperation("验收通过/结案,同步联动隐患状态为已整改")
-    public AjaxResult close(@RequestBody Map<String, Object> body) {
-        Long orderId = Long.valueOf(body.get("orderId").toString());
-        String result = body.get("result") != null ? body.get("result").toString() : "";
-        Long userId = SecurityUtils.getUserId();
-        String userName = SecurityUtils.getUsername();
-        businessWorkOrderService.closeOrder(orderId, result, userId, userName);
-        return AjaxResult.success("验收通过");
+    @ApiOperation("验收通过/结案")
+    public AjaxResult close(@RequestBody WorkOrderOperateDTO dto) {
+        WorkOrderVerifyDTO verifyDTO = new WorkOrderVerifyDTO();
+        verifyDTO.setOrderId(dto.getOrderId());
+        verifyDTO.setPassed(Boolean.TRUE);
+        verifyDTO.setContent(dto.getContent());
+        verifyDTO.setAttachUrl(dto.getAttachUrl());
+        return businessWorkOrderService.verify(verifyDTO)
+                ? AjaxResult.success("验收通过") : AjaxResult.error("验收失败");
     }
 
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:cancel')")
+    @PutMapping("/reject")
+    @ApiOperation("驳回工单(历史接口,等价取消)")
+    public AjaxResult reject(@RequestBody WorkOrderOperateDTO dto) {
+        return businessWorkOrderService.cancel(dto)
+                ? AjaxResult.success("驳回成功") : AjaxResult.error("驳回失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:delay')")
+    @PutMapping("/delay")
+    @ApiOperation("申请延期")
+    public AjaxResult delay(@RequestBody WorkOrderDelayApplyDTO dto) {
+        return businessWorkOrderService.applyDelay(dto)
+                ? AjaxResult.success("延期申请已提交") : AjaxResult.error("延期申请失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:cancel')")
+    @DeleteMapping("/{orderId}")
+    @ApiOperation("取消工单")
+    public AjaxResult cancel(@PathVariable Long orderId, @RequestBody(required = false) WorkOrderOperateDTO dto) {
+        WorkOrderOperateDTO cancelDTO = dto == null ? new WorkOrderOperateDTO() : dto;
+        cancelDTO.setOrderId(orderId);
+        return businessWorkOrderService.cancel(cancelDTO)
+                ? AjaxResult.success("取消工单成功") : AjaxResult.error("取消工单失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:add')")
     @PostMapping("/createFromHazard")
     @ApiOperation("从隐患创建工单")
     public AjaxResult createFromHazard(@RequestBody Map<String, Object> body) {
-        String hazardId = (String) body.get("hazardId");
-        String hazardNo = (String) body.get("hazardNo");
-        String hazardDesc = (String) body.get("hazardDesc");
-        String equipmentId = (String) body.get("equipmentId");
-        String hazardLevel = (String) body.get("hazardLevel");
-        String hazardImages = (String) body.get("hazardImages");
-        String planFinishTimeStr = (String) body.get("planFinishTime");
-
-        if (hazardId == null || equipmentId == null) {
-            return AjaxResult.error("隐患ID和设备ID不能为空");
-        }
-
-        Date planFinishTime = null;
-        if (StringUtils.isNotEmpty(planFinishTimeStr)) {
-            planFinishTime = DateUtils.parseDate(planFinishTimeStr);
-        }
-
-        Long orderId = businessWorkOrderService.createWorkOrderFromHazard(hazardId, hazardNo, hazardDesc, equipmentId, hazardLevel, hazardImages, planFinishTime);
+        String planFinishTimeStr = toStringValue(body.get("planFinishTime"));
+        Date planFinishTime = StringUtils.isNotEmpty(planFinishTimeStr) ? DateUtils.parseDate(planFinishTimeStr) : null;
+        Long orderId = businessWorkOrderService.createWorkOrderFromHazard(
+                toStringValue(body.get("hazardId")),
+                toStringValue(body.get("hazardNo")),
+                toStringValue(body.get("hazardDesc")),
+                toStringValue(body.get("equipmentId")),
+                toStringValue(body.get("hazardLevel")),
+                toStringValue(body.get("hazardImages")),
+                planFinishTime);
         return AjaxResult.success("工单创建成功", orderId);
     }
 
-    @PutMapping("/reject")
-    @ApiOperation("驳回工单(待派单/待接单 -> 已驳回)")
-    public AjaxResult reject(@RequestBody Map<String, Object> body) {
-        Long orderId = Long.valueOf(body.get("orderId").toString());
-        String reason = body.get("reason") != null ? body.get("reason").toString() : "";
-        Long userId = SecurityUtils.getUserId();
-        String userName = SecurityUtils.getUsername();
-
-        businessWorkOrderService.rejectOrder(orderId, reason, userId, userName);
-        return AjaxResult.success("驳回成功");
+    private Long toLong(Object value) {
+        return value == null ? null : Long.valueOf(value.toString());
     }
 
-    @DeleteMapping("/{orderId}")
-    @ApiOperation("取消工单")
-    public AjaxResult cancel(@PathVariable Long orderId) {
-        Long userId = SecurityUtils.getUserId();
-        String userName = SecurityUtils.getUsername();
-
-        businessWorkOrderService.cancelOrder(orderId, userId, userName);
-        return AjaxResult.success("取消工单成功");
+    private String toStringValue(Object value) {
+        return value == null ? null : String.valueOf(value);
     }
 }

+ 183 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/ops/PworkOrderController.java

@@ -0,0 +1,183 @@
+package com.zksy.web.controller.ops;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.enums.WorkOrderStatusEnum;
+import com.zksy.base.service.BusinessWorkOrderService;
+import com.zksy.common.annotation.DataScope;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.core.domain.TreeSelect;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.enums.BusinessType;
+import com.zksy.ops.dto.WorkOrderDelayApplyDTO;
+import com.zksy.ops.dto.WorkOrderDelayAuditDTO;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
+import com.zksy.ops.dto.WorkOrderOperateDTO;
+import com.zksy.ops.dto.WorkOrderPageDTO;
+import com.zksy.ops.dto.WorkOrderVerifyDTO;
+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.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 运维管理-故障工单
+ *
+ * @author zksy
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/ops/pworkOrder")
+@Api(tags = "运维管理-故障工单")
+public class PworkOrderController {
+
+    @Autowired
+    private BusinessWorkOrderService workOrderService;
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:query')")
+    @GetMapping("/findByPage")
+    @ApiOperation("分页查询工单列表")
+    @DataScope(deptAlias = "app_user.work_order", permission = "ops:pworkOrder:query")
+    public AjaxResult findByPage(WorkOrderPageDTO dto) {
+        Page<WorkOrder> page = workOrderService.findByPage(dto);
+        Map<String, Object> data = new HashMap<>(4);
+        data.put("records", page.getRecords());
+        data.put("total", page.getTotal());
+        data.put("pageNum", page.getCurrent());
+        data.put("pageSize", page.getSize());
+        return AjaxResult.success(data);
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:query')")
+    @GetMapping("/getById/{orderId}")
+    @ApiOperation("获取工单详情(含操作日志)")
+    public AjaxResult getById(@PathVariable("orderId") Long orderId) {
+        return AjaxResult.success(workOrderService.getDetailById(orderId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:query')")
+    @GetMapping("/logs/{orderId}")
+    @ApiOperation("查询工单操作日志")
+    public AjaxResult logs(@PathVariable("orderId") Long orderId) {
+        List<WorkOrderLog> logs = workOrderService.getWorkOrderLogs(orderId);
+        return AjaxResult.success(logs);
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:query')")
+    @GetMapping("/assignableDeptTree")
+    @ApiOperation("获取可指派部门树")
+    public AjaxResult assignableDeptTree() {
+        List<TreeSelect> tree = workOrderService.assignableDeptTree();
+        return AjaxResult.success(tree);
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:query')")
+    @GetMapping("/assignableUsers")
+    @ApiOperation("查询可指派用户")
+    public AjaxResult assignableUsers(@RequestParam(value = "deptId", required = false) Long deptId) {
+        List<SysUser> users = workOrderService.assignableUsers(deptId);
+        return AjaxResult.success(users);
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:add')")
+    @PostMapping("/add")
+    @ApiOperation("新增故障工单(初始状态:待派单)")
+    @Log(title = "新增故障工单", businessType = BusinessType.INSERT)
+    public AjaxResult add(@Valid @RequestBody WorkOrder workOrder) {
+        Long orderId = workOrderService.addWorkOrder(workOrder);
+        return AjaxResult.success("工单创建成功", orderId);
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:dispatch')")
+    @PutMapping("/dispatch")
+    @ApiOperation("派单(待派单 → 待接单)")
+    @Log(title = "工单派单", businessType = BusinessType.UPDATE)
+    public AjaxResult dispatch(@Valid @RequestBody WorkOrderDispatchDTO dto) {
+        return workOrderService.dispatch(dto) ? AjaxResult.success("派单成功") : AjaxResult.error("派单失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:accept')")
+    @PutMapping("/accept")
+    @ApiOperation("核查接单(待接单 → 已接单)")
+    @Log(title = "工单接单", businessType = BusinessType.UPDATE)
+    public AjaxResult accept(@Valid @RequestBody WorkOrderOperateDTO dto) {
+        return workOrderService.accept(dto) ? AjaxResult.success("接单成功") : AjaxResult.error("接单失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:process')")
+    @PutMapping("/process")
+    @ApiOperation("工单处置(已接单/处理中/延期中 → 处理中,可多次处置)")
+    @Log(title = "工单处置", businessType = BusinessType.UPDATE)
+    public AjaxResult process(@Valid @RequestBody WorkOrderOperateDTO dto) {
+        return workOrderService.process(dto) ? AjaxResult.success("处置成功") : AjaxResult.error("处置失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:submitVerify')")
+    @PutMapping("/submitVerify")
+    @ApiOperation("提交验收(处理中/延期中 → 待验收)")
+    @Log(title = "工单提交验收", businessType = BusinessType.UPDATE)
+    public AjaxResult submitVerify(@Valid @RequestBody WorkOrderOperateDTO dto) {
+        return workOrderService.submitVerify(dto) ? AjaxResult.success("提交验收成功") : AjaxResult.error("提交验收失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:verify')")
+    @PutMapping("/verify")
+    @ApiOperation("工单验收(待验收 → 已结案 / 处理中)")
+    @Log(title = "工单验收", businessType = BusinessType.UPDATE)
+    public AjaxResult verify(@Valid @RequestBody WorkOrderVerifyDTO dto) {
+        return workOrderService.verify(dto) ? AjaxResult.success("验收完成") : AjaxResult.error("验收失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:cancel')")
+    @PutMapping("/cancel")
+    @ApiOperation("取消工单(待派单/待接单/已接单/处理中/待验收 → 已取消)")
+    @Log(title = "取消工单", businessType = BusinessType.UPDATE)
+    public AjaxResult cancel(@Valid @RequestBody WorkOrderOperateDTO dto) {
+        return workOrderService.cancel(dto) ? AjaxResult.success("取消成功") : AjaxResult.error("取消失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:delay')")
+    @PutMapping("/delay/apply")
+    @ApiOperation("申请延期(处理中且无延期申请 → 待延期审核)")
+    @Log(title = "工单申请延期", businessType = BusinessType.UPDATE)
+    public AjaxResult applyDelay(@Valid @RequestBody WorkOrderDelayApplyDTO dto) {
+        return workOrderService.applyDelay(dto) ? AjaxResult.success("延期申请已提交") : AjaxResult.error("延期申请失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:delayAudit')")
+    @PutMapping("/delay/audit")
+    @ApiOperation("延期审核(待延期审核 → 延期审核通过 / 延期审核驳回)")
+    @Log(title = "工单延期审核", businessType = BusinessType.UPDATE)
+    public AjaxResult auditDelay(@Valid @RequestBody WorkOrderDelayAuditDTO dto) {
+        return workOrderService.auditDelay(dto) ? AjaxResult.success("延期审核完成") : AjaxResult.error("延期审核失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('ops:pworkOrder:remove')")
+    @DeleteMapping("/deleteById/{orderId}")
+    @ApiOperation("删除工单(仅限已取消工单)")
+    @Log(title = "删除工单", businessType = BusinessType.DELETE)
+    public AjaxResult deleteById(@PathVariable("orderId") Long orderId) {
+        WorkOrder order = workOrderService.getById(orderId);
+        if (order != null && !WorkOrderStatusEnum.CANCELLED.getCode().equals(order.getOrderStatus())) {
+            return AjaxResult.error("仅已取消工单允许删除");
+        }
+        return workOrderService.deleteWithLogs(orderId) ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+    }
+}

+ 9 - 9
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalControllerTest.java

@@ -10,6 +10,7 @@ import com.zksy.common.core.domain.entity.SysUser;
 import com.zksy.common.core.domain.model.LoginUser;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.junit.jupiter.api.BeforeEach;
@@ -78,7 +79,7 @@ class DeviceAbnormalControllerTest {
         order.setOrderNo("WO-T6-001");
         order.setOrderStatus(1);
         when(deviceService.createAbnormalWorkOrder(any())).thenReturn(order);
-        when(businessWorkOrderService.dispatchOrder(12L, 88L, null, 7L, "operator")).thenReturn(true);
+        when(businessWorkOrderService.dispatch(any(WorkOrderDispatchDTO.class))).thenReturn(true);
 
         mockMvc.perform(post("/waterSupply/device/abnormal/order")
                         .contentType("application/json")
@@ -89,10 +90,10 @@ class DeviceAbnormalControllerTest {
                     assertTrue(body.contains("WO-T6-001"));
                     assertTrue(body.contains("orderId"));
                     assertTrue(body.contains("orderStatus"));
-                    assertTrue(body.contains("\"orderStatus\":2"));
-                    assertTrue(body.contains("\"receiveUser\":\"88\""));
+                    assertTrue(body.contains("\"orderStatus\":1"));
+                    assertTrue(body.contains("\"receiveUser\":88"));
                 });
-        verify(businessWorkOrderService).dispatchOrder(12L, 88L, null, 7L, "operator");
+        verify(businessWorkOrderService).dispatch(any(WorkOrderDispatchDTO.class));
     }
 
     @Test
@@ -112,10 +113,9 @@ class DeviceAbnormalControllerTest {
         }}.createAbnormalOrder(request);
 
         assertEquals(200, result.get("code"));
-        assertEquals(1, ((WorkOrder) result.get("data")).getOrderStatus());
-        verify(businessWorkOrderService, org.mockito.Mockito.never())
-                .dispatchOrder(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
-                        org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+        assertEquals(0, ((WorkOrder) result.get("data")).getOrderStatus());
+        org.mockito.Mockito.verify(businessWorkOrderService, org.mockito.Mockito.never())
+                .dispatch(org.mockito.ArgumentMatchers.any(WorkOrderDispatchDTO.class));
     }
 
     @Test
@@ -139,7 +139,7 @@ class DeviceAbnormalControllerTest {
         order.setOrderNo("WO-T6-002");
         order.setOrderStatus(1);
         when(deviceService.createAbnormalWorkOrder(any())).thenReturn(order);
-        when(businessWorkOrderService.dispatchOrder(13L, 88L, null, 7L, "operator")).thenReturn(false);
+        when(businessWorkOrderService.dispatch(any(WorkOrderDispatchDTO.class))).thenReturn(false);
 
         mockMvc.perform(post("/waterSupply/device/abnormal/order")
                         .contentType("application/json")

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

@@ -327,7 +327,7 @@ class CockpitEquipmentDetailAggregateControllerTest {
 
     private CockpitWorkOrderSummary workOrderSummary() {
         return new CockpitWorkOrderSummary(
-                "WO-2026090200001", DEVICE_CODE, 1, "故障维修", 1, "处理异常", 4, "处理中",
+                "WO-2026090200001", DEVICE_CODE, 1, "处理异常", 4, "处理中",
                 "2026-09-02T09:00:00+08:00", "2026-09-02T10:00:00+08:00",
                 "2026-09-03T09:00:00+08:00", null,
                 new CockpitWorkOrderEventAssociation("ALARM", "ALM-20260902-001", "2026-09-02T10:00:00+08:00"));

+ 1 - 2
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitFreshnessSemanticsControllerTest.java

@@ -36,8 +36,7 @@ class CockpitFreshnessSemanticsControllerTest {
         adapter = mock(CockpitFreshnessAdapter.class);
         CockpitFreshnessService service = new CockpitFreshnessService(
                 adapter,
-                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC),
-                60);
+                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
         mockMvc = MockMvcBuilders
                 .standaloneSetup(new CockpitFreshnessController(service, Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC)))
                 .build();

+ 0 - 2
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitWorkOrderReadControllerTest.java

@@ -228,8 +228,6 @@ class CockpitWorkOrderReadControllerTest {
         return new CockpitWorkOrderSummary(
                 workOrderCode,
                 DEVICE_CODE,
-                1,
-                "故障维修",
                 2,
                 "处理异常",
                 stateEntry.getCode(),

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

@@ -333,22 +333,21 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
         workOrder.setAlarmId(alarm.getId());
         workOrder.setDeviceId(equipment != null ? equipment.getEquipmentId() : alarm.getDeviceCode());
         workOrder.setDeviceCode(alarm.getDeviceCode());
-        workOrder.setOrderType(inDTO.getOrderType() == null ? 1 : inDTO.getOrderType());
         workOrder.setOrderLevel(inDTO.getOrderLevel() == null
                 ? mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()) : inDTO.getOrderLevel());
         workOrder.setOrderDesc(StrUtil.isNotBlank(inDTO.getOrderDesc()) ? inDTO.getOrderDesc()
                 : buildDefaultOrderDesc(alarm));
         workOrder.setDeptId(inDTO.getDeptId());
         workOrder.setPlanFinishTime(inDTO.getPlanFinishTime());
-        workOrder.setOrderStatus(1); // 1-待派单
+        workOrder.setOrderStatus(0); // 0-待派单
         workOrder.setCreateTime(new java.util.Date());
         workOrder.setUpdateTime(new java.util.Date());
         workOrderMapper.insert(workOrder);
 
-        // 写工单操作日志(1-创建工单)
+        // 写工单操作日志(0-创建工单)
         WorkOrderLog workOrderLog = new WorkOrderLog();
         workOrderLog.setOrderId(workOrder.getOrderId());
-        workOrderLog.setOperType(1);
+        workOrderLog.setOperType(0);
         workOrderLog.setOperDesc("由供水监测报警[" + alarm.getWarningType() + "]派发工单:" + workOrder.getOrderNo());
         workOrderLog.setOperUserId(currentUserId());
         workOrderLog.setOperUserName(currentUserName());
@@ -366,16 +365,16 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     }
 
     /**
-     * 报警等级 -> 工单优先级:1级→紧急,2/3级→一般,4级→低
+     * 报警等级 -> 工单优先级:1级→紧急(0),2/3级→一般(1),4级→低(2)
      */
     private Integer mapAlarmLevelToOrderLevel(Integer alarmLevel) {
         if (alarmLevel == null || alarmLevel == 1) {
-            return 1;
+            return 0;
         }
         if (alarmLevel == 4) {
-            return 3;
+            return 2;
         }
-        return 2;
+        return 1;
     }
 
     private String buildDefaultOrderDesc(AlarmData alarm) {
@@ -445,7 +444,6 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
             // 审核通过即上传至运维工单,工单默认待派单状态
             AlarmDispatchInDTO dispatch = new AlarmDispatchInDTO();
             dispatch.setAlarmId(alarm.getId());
-            dispatch.setOrderType(1);
             dispatch.setOrderLevel(mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()));
             dispatch.setOrderDesc(StrUtil.isBlank(inDTO.getAuditOpinion())
                     ? null : inDTO.getAuditOpinion());

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

@@ -103,6 +103,18 @@ public class AlarmData implements Serializable {
     @ApiModelProperty(value = "纬度")
     private String latitude;
 
+    /** 是否已经关联运维工单(由查询接口补充) */
+    @TableField(exist = false)
+    private Boolean hasWorkOrder;
+
+    /** 关联工单ID(由查询接口补充) */
+    @TableField(exist = false)
+    private Long workOrderId;
+
+    /** 审核状态:1待审核,2已审核(由报警和工单状态综合计算) */
+    @TableField(exist = false)
+    private Integer auditStatus;
+
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;
 }

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

@@ -8,6 +8,7 @@ import com.zksy.base.alarm.mapper.AlarmDataMapper;
 import com.zksy.base.alarm.service.AlarmDataService;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -22,6 +23,9 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
     @Autowired
     private EquipmentBaseMapper equipmentBaseMapper;
 
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
     @Override
     public Page<AlarmData> findByPage(long pageNum, long pageSize, String deviceCode,
                                         String warningType, String warningCode,
@@ -63,10 +67,41 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
 
         // 填充 deviceName/longitude/latitude:通过 device_code 从 equipment_base 表关联查询
         fillDeviceInfo(result.getRecords());
+        // 报警可能已经创建工单但 alarm_status 未同步,列表状态以工单关联为准补齐。
+        fillWorkOrderStatus(result.getRecords());
 
         return result;
     }
 
+    private void fillWorkOrderStatus(List<AlarmData> records) {
+        if (records == null || records.isEmpty()) {
+            return;
+        }
+        List<String> alarmIds = records.stream()
+                .map(AlarmData::getId)
+                .filter(id -> id != null && !id.isEmpty())
+                .distinct()
+                .collect(Collectors.toList());
+        if (alarmIds.isEmpty()) {
+            return;
+        }
+        List<com.zksy.base.domain.WorkOrder> orders = workOrderMapper.selectList(
+                new LambdaQueryWrapper<com.zksy.base.domain.WorkOrder>()
+                        .in(com.zksy.base.domain.WorkOrder::getAlarmId, alarmIds)
+                        .orderByDesc(com.zksy.base.domain.WorkOrder::getOrderId));
+        java.util.Map<String, com.zksy.base.domain.WorkOrder> orderMap = orders.stream()
+                .filter(order -> order.getAlarmId() != null)
+                .collect(Collectors.toMap(com.zksy.base.domain.WorkOrder::getAlarmId,
+                        order -> order, (first, ignored) -> first));
+        for (AlarmData record : records) {
+            com.zksy.base.domain.WorkOrder order = orderMap.get(record.getId());
+            boolean hasOrder = order != null;
+            record.setHasWorkOrder(hasOrder);
+            record.setWorkOrderId(hasOrder ? order.getOrderId() : null);
+            record.setAuditStatus(hasOrder || Integer.valueOf(1).equals(record.getAlarmStatus()) ? 2 : 1);
+        }
+    }
+
     private LocalDateTime parseDateTime(String value) {
         return LocalDateTime.parse(value, java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
     }

+ 19 - 122
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/WorkOrder.java

@@ -14,7 +14,6 @@ import java.util.Date;
 
 /**
  * 运维工单表
- * @TableName work_order
  */
 @TableName(value = "work_order", schema = "app_user")
 @Data
@@ -23,180 +22,78 @@ public class WorkOrder implements Serializable {
 
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;
-
-    /**
-     * 工单ID
-     */
     @ApiModelProperty(value = "工单ID")
     @TableId(type = IdType.AUTO)
     private Long orderId;
-
-    /**
-     * 工单编号
-     */
     @ApiModelProperty(value = "工单编号")
     private String orderNo;
-
-    /**
-     * 关联报警ID
-     */
     @ApiModelProperty(value = "报警ID")
     private String alarmId;
-
-    /**
-     * 关联案件ID
-     */
     @ApiModelProperty(value = "案件ID")
     private Long caseId;
-
-    /**
-     * 案件编号
-     */
     @ApiModelProperty(value = "案件编号")
     private String caseNo;
-
-    /**
-     * 关联隐患ID
-     */
     @ApiModelProperty(value = "隐患ID")
     private String hazardId;
-
-    /**
-     * 隐患编号
-     */
     @ApiModelProperty(value = "隐患编号")
     private String hazardNo;
-
-    /**
-     * 关联设备ID
-     */
     @ApiModelProperty(value = "设备ID")
     private String deviceId;
-
-    /**
-     * 设备编码
-     */
     @ApiModelProperty(value = "设备编码")
     private String deviceCode;
-
-    /**
-     * 工单类型:1-故障维修 2-日常巡检 3-设备保养
-     */
-    @ApiModelProperty(value = "工单类型:1-故障维修 2-日常巡检 3-设备保养")
-    private Integer orderType;
-
-    /**
-     * 工单优先级:1-紧急 2-一般 3-低
-     */
-    @ApiModelProperty(value = "工单优先级:1-紧急 2-一般 3-低")
+    @ApiModelProperty(value = "故障发生时间")
+    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date faultTime;
+    @ApiModelProperty(value = "工单优先级:0-紧急 1-一般 2-低")
     private Integer orderLevel;
-
-    /**
-     * 工单问题描述
-     */
     @ApiModelProperty(value = "工单问题描述")
     private String orderDesc;
-
-    /**
-     * 负责部门ID
-     */
-    @ApiModelProperty(value = "负责部门ID")
+    @ApiModelProperty(value = "归属部门ID,取派单时指定责任人所属部门;待派单阶段为创建人部门。数据权限按此字段做「本部门及子部门」过滤")
     private Long deptId;
-
-    /**
-     * 派单人ID
-     */
     @ApiModelProperty(value = "派单人ID")
     private Long dispatchUser;
-
-    /**
-     * 派单时间
-     */
     @ApiModelProperty(value = "派单时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date dispatchTime;
-
-    /**
-     * 接单运维人员ID(多个用逗号分隔)
-     */
-    @ApiModelProperty(value = "接单运维人员ID,多个用逗号分隔")
-    private String receiveUser;
-
-    /**
-     * 工单状态:1-待派单 2-待接单 3-已接单 4-处理中 5-待验收 6-已结案 7-已驳回 8-延期审核 9-已延期
-     */
-    @ApiModelProperty(value = "工单状态:1-待派单 2-待接单 3-已接单 4-处理中 5-待验收 6-已结案 7-已驳回 8-延期审核 9-已延期")
+    @ApiModelProperty(value = "责任人用户ID")
+    private Long receiveUser;
+    @ApiModelProperty(value = "工单状态:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消 (见WorkOrderStatusEnum)")
     private Integer orderStatus;
-
-    /**
-     * 延期原因
-     */
+    @ApiModelProperty(value = "延期申请状态:0-无延期申请 1-待延期审核 2-延期审核通过 3-延期审核驳回 见 WorkOrderDelayApplyStatusEnum)")
+    private Integer delayApplyStatus;
     @ApiModelProperty(value = "延期原因")
     private String delayedReason;
-
-    /**
-     * 延期截止时间
-     */
     @ApiModelProperty(value = "延期截止时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date delayedDeadline;
-
-    /**
-     * 计划完成时间
-     */
     @ApiModelProperty(value = "计划完成时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date planFinishTime;
-
-    /**
-     * 实际完成时间
-     */
     @ApiModelProperty(value = "实际完成时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date realFinishTime;
-
-    /**
-     * 维修处置结果
-     */
     @ApiModelProperty(value = "维修处置结果")
     private String repairResult;
-
-    /**
-     * 维修前图片 URL,多个图片使用逗号分隔
-     */
-    @ApiModelProperty(value = "维修前图片 URL")
+    @ApiModelProperty(value = "维修前图片URL,多个图片使用逗号分隔")
     private String maintenanceBeforeImg;
-
-    /**
-     * 维修后图片 URL,多个图片使用逗号分隔
-     */
-    @ApiModelProperty(value = "维修后图片 URL")
+    @ApiModelProperty(value = "维修后图片URL,多个图片使用逗号分隔")
     private String maintenanceAfterImg;
-
-    /**
-     * 验收人ID
-     */
     @ApiModelProperty(value = "验收人ID")
     private Long verifyUser;
-
-    /**
-     * 验收时间
-     */
     @ApiModelProperty(value = "验收时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date verifyTime;
-
-    /**
-     * 创建时间
-     */
     @ApiModelProperty(value = "创建时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date createTime;
-
-    /**
-     * 更新时间
-     */
     @ApiModelProperty(value = "更新时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date updateTime;
+
+    @ApiModelProperty(value = "责任人姓名")
+    @TableField(exist = false)
+    private String receiveUserName;
+    @ApiModelProperty(value = "派单人姓名")
+    @TableField(exist = false)
+    private String dispatchUserName;
 }

+ 24 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/WorkOrderLog.java

@@ -11,10 +11,10 @@ import lombok.Data;
 
 import java.io.Serializable;
 import java.util.Date;
+import java.util.List;
 
 /**
  * 工单操作日志表
- * @TableName work_order_log
  */
 @TableName(value = "work_order_log", schema = "app_user")
 @Data
@@ -38,17 +38,38 @@ public class WorkOrderLog implements Serializable {
     private Long orderId;
 
     /**
-     * 操作类型:1-创建工单 2-派单 3-接单 4-开始维修 5-提交验收 6-验收通过 7-驳回工单 8-申请延期
+     * 操作类型:0-创建工单 1-派单 2-接单 3-处置 4-提交验收 5-验收通过 6-验收驳回 7-取消工单
+     * 8-申请延期 9-延期审核通过 10-延期审核驳回(见 WorkOrderLogOperTypeEnum)
      */
-    @ApiModelProperty(value = "操作类型:1-创建工单 2-派单 3-接单 4-开始维修 5-提交验收 6-验收通过 7-驳回工单 8-申请延期 9-延期审核通过")
+    @ApiModelProperty(value = "操作类型:0-创建工单 1-派单 2-接单 3-处置 4-提交验收 5-验收通过 6-验收驳回 7-取消工单 8-申请延期 9-延期审核通过 10-延期审核驳回")
     private Integer operType;
 
+    /**
+     * 操作类型描述(非数据库字段,查询时回填)
+     */
+    @ApiModelProperty(value = "操作类型描述")
+    @TableField(exist = false)
+    private String operTypeDesc;
+
     /**
      * 操作详情描述
      */
     @ApiModelProperty(value = "操作详情描述")
     private String operDesc;
 
+    /**
+     * 操作附件 URL,多个使用逗号分隔
+     */
+    @ApiModelProperty(value = "操作附件 URL,多个使用逗号分隔")
+    private String attachUrl;
+
+    /**
+     * 操作附件 URL 列表(非数据库字段,查询时回填,便于前端预览)
+     */
+    @ApiModelProperty(value = "操作附件列表")
+    @TableField(exist = false)
+    private List<String> attachList;
+
     /**
      * 操作人ID
      */

+ 6 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/WorkOrderPushVO.java

@@ -24,16 +24,19 @@ public class WorkOrderPushVO implements Serializable {
     @ApiModelProperty("工单编号")
     private String orderNo;
 
-    @ApiModelProperty("工单状态:1-待派单 2-待接单 3-已接单 4-处理中 5-待验收 8-延期审核 9-已延期")
+    @ApiModelProperty("工单状态:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消")
     private Integer orderStatus;
 
-    @ApiModelProperty("工单优先级")
+    @ApiModelProperty("工单状态描述")
+    private String orderStatusDesc;
+
+    @ApiModelProperty("工单优先级:1-紧急 2-一般 3-低")
     private Integer orderLevel;
 
     @ApiModelProperty("所属部门ID")
     private Long deptId;
 
-    @ApiModelProperty("接单人ID,多个使用逗号分隔")
+    @ApiModelProperty("责任人用户ID")
     private String receiveUser;
 
     @ApiModelProperty("设备ID")

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

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.domain.CaseInfo;
 import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.enums.WorkOrderLogOperTypeEnum;
+import com.zksy.base.enums.WorkOrderStatusEnum;
 import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.WorkOrderLog;
 import com.zksy.base.mapper.CaseInfoMapper;
@@ -54,7 +56,7 @@ public class DrillAlarmSyncService {
         String typeId = equipmentBase.getEquipmentTypeId();
         boolean needCreateCase = CASE_REQUIRED_TYPE_IDS.contains(typeId);
         int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : 4;
-        int orderLevel = caseLevel <= 2 ? 1 : (caseLevel == 3 ? 2 : 3);
+        int orderLevel = caseLevel <= 2 ? 0 : (caseLevel == 3 ? 1 : 2);
         String desc = alarmData.getWarningType() != null ? alarmData.getWarningType() : "设备告警";
         if (alarmData.getRemark() != null) {
             desc += "-" + alarmData.getRemark();
@@ -78,10 +80,9 @@ public class DrillAlarmSyncService {
         workOrder.setAlarmId(alarmData.getId());
         workOrder.setDeviceId(equipmentBase.getEquipmentId());
         workOrder.setDeviceCode(alarmData.getDeviceCode());
-        workOrder.setOrderType(1);
         workOrder.setOrderLevel(orderLevel);
         workOrder.setOrderDesc(desc);
-        workOrder.setOrderStatus(1);
+        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
         if (caseInfo != null) {
             workOrder.setCaseId(caseInfo.getCaseId());
             workOrder.setCaseNo(caseInfo.getCaseNo());

+ 52 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderDelayApplyStatusEnum.java

@@ -0,0 +1,52 @@
+package com.zksy.base.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * 工单延期申请状态枚举(work_order.delay_apply_status)
+ *
+ * @author zksy
+ */
+@Getter
+@AllArgsConstructor
+public enum WorkOrderDelayApplyStatusEnum {
+
+    /** 无延期申请 */
+    NONE(0, "无延期申请"),
+    /** 待延期审核 */
+    PENDING_AUDIT(1, "待延期审核"),
+    /** 延期审核通过 */
+    APPROVED(2, "延期审核通过"),
+    /** 延期审核驳回 */
+    REJECTED(3, "延期审核驳回");
+
+    private final Integer code;
+
+    private final String desc;
+
+    /**
+     * 按 code 获取枚举
+     */
+    public static WorkOrderDelayApplyStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (WorkOrderDelayApplyStatusEnum item : values()) {
+            if (Objects.equals(item.code, code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 按 code 获取描述,未知返回 "未知"
+     */
+    public static String getDescByCode(Integer code) {
+        WorkOrderDelayApplyStatusEnum item = getByCode(code);
+        return item == null ? "未知" : item.getDesc();
+    }
+}

+ 60 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderLevelEnum.java

@@ -0,0 +1,60 @@
+package com.zksy.base.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * 工单优先级枚举(work_order.order_level)
+ *
+ * @author zksy
+ */
+@Getter
+@AllArgsConstructor
+public enum WorkOrderLevelEnum {
+
+    /** 紧急 */
+    URGENT(0, "紧急"),
+    /** 一般 */
+    NORMAL(1, "一般"),
+    /** 低 */
+    LOW(2, "低");
+
+    private final Integer code;
+
+    private final String desc;
+
+    /** 默认优先级 */
+    public static final WorkOrderLevelEnum DEFAULT = NORMAL;
+
+    /**
+     * 按 code 获取枚举,非法值返回默认优先级
+     */
+    public static WorkOrderLevelEnum getByCode(Integer code) {
+        if (code == null) {
+            return DEFAULT;
+        }
+        for (WorkOrderLevelEnum item : values()) {
+            if (Objects.equals(item.code, code)) {
+                return item;
+            }
+        }
+        return DEFAULT;
+    }
+
+    /**
+     * 按 code 获取描述
+     */
+    public static String getDescByCode(Integer code) {
+        return getByCode(code).getDesc();
+    }
+
+    /**
+     * 是否合法优先级
+     */
+    public static boolean isValid(Integer code) {
+        return code != null && (Objects.equals(URGENT.code, code)
+                || Objects.equals(NORMAL.code, code) || Objects.equals(LOW.code, code));
+    }
+}

+ 68 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderLogOperTypeEnum.java

@@ -0,0 +1,68 @@
+package com.zksy.base.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * 工单操作日志类型枚举(work_order_log.oper_type)
+ *
+ * <p>与工单处置流程一一对应,从 0 开始编号</p>
+ *
+ * @author zksy
+ */
+@Getter
+@AllArgsConstructor
+public enum WorkOrderLogOperTypeEnum {
+
+    /** 创建工单 */
+    CREATE(0, "创建工单"),
+    /** 派单 */
+    DISPATCH(1, "派单"),
+    /** 核查接单 */
+    ACCEPT(2, "接单"),
+    /** 处置(处理) */
+    PROCESS(3, "处置"),
+    /** 提交验收 */
+    SUBMIT_VERIFY(4, "提交验收"),
+    /** 验收通过 */
+    VERIFY_PASS(5, "验收通过"),
+    /** 验收驳回 */
+    VERIFY_REJECT(6, "验收驳回"),
+    /** 取消工单 */
+    CANCEL(7, "取消工单"),
+    /** 申请延期 */
+    DELAY_APPLY(8, "申请延期"),
+    /** 延期审核通过 */
+    DELAY_APPROVE(9, "延期审核通过"),
+    /** 延期审核驳回 */
+    DELAY_REJECT(10, "延期审核驳回");
+
+    private final Integer code;
+
+    private final String desc;
+
+    /**
+     * 按 code 获取枚举
+     */
+    public static WorkOrderLogOperTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (WorkOrderLogOperTypeEnum item : values()) {
+            if (Objects.equals(item.code, code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 按 code 获取操作描述,未知返回 "操作"
+     */
+    public static String getDescByCode(Integer code) {
+        WorkOrderLogOperTypeEnum item = getByCode(code);
+        return item == null ? "操作" : item.getDesc();
+    }
+}

+ 146 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderStatusEnum.java

@@ -0,0 +1,146 @@
+package com.zksy.base.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 运维工单状态枚举(work_order.order_status)
+ *
+ * <p>流程:0待派单 → 1待接单 → 2已接单 → 3处理中 → 4待验收 → 5已结案</p>
+ * <p>旁路:6延期中(延期申请审核通过)、7已取消</p>
+ *
+ * @author zksy
+ */
+@Getter
+@AllArgsConstructor
+public enum WorkOrderStatusEnum {
+
+    /** 刚创建、未分配责任人 */
+    PENDING_DISPATCH(0, "待派单"),
+    /** 已派单,等待责任人核查接单 */
+    PENDING_ACCEPT(1, "待接单"),
+    /** 责任人已核查接单 */
+    ACCEPTED(2, "已接单"),
+    /** 责任人处置中 */
+    PROCESSING(3, "处理中"),
+    /** 已提交验收,等待验收人验收 */
+    PENDING_VERIFY(4, "待验收"),
+    /** 验收通过,工单结案 */
+    CLOSED(5, "已结案"),
+    /** 延期申请审核通过,工单延期处理中 */
+    DELAYING(6, "延期中"),
+    /** 工单已取消 */
+    CANCELLED(7, "已取消");
+
+    private final Integer code;
+
+    private final String desc;
+
+    /** 允许取消的状态:待派单/待接单/已接单/处理中/待验收 */
+    private static final List<Integer> CANCELABLE_CODES =
+            Collections.unmodifiableList(Arrays.asList(
+                    PENDING_DISPATCH.code, PENDING_ACCEPT.code, ACCEPTED.code,
+                    PROCESSING.code, PENDING_VERIFY.code));
+
+    /** 允许处置(处理)的状态:已接单/处理中/延期中 */
+    private static final List<Integer> PROCESSABLE_CODES =
+            Collections.unmodifiableList(Arrays.asList(
+                    ACCEPTED.code, PROCESSING.code, DELAYING.code));
+
+    /** 允许提交验收的状态:处理中/延期中 */
+    private static final List<Integer> SUBMIT_VERIFY_CODES =
+            Collections.unmodifiableList(Arrays.asList(PROCESSING.code, DELAYING.code));
+
+    /** 未办结状态(统计口径) */
+    private static final List<Integer> ACTIVE_CODES =
+            Collections.unmodifiableList(Arrays.asList(
+                    PENDING_DISPATCH.code, PENDING_ACCEPT.code, ACCEPTED.code,
+                    PROCESSING.code, PENDING_VERIFY.code, DELAYING.code));
+
+    /** 终态:已结案、已取消 */
+    private static final List<Integer> FINISHED_CODES =
+            Collections.unmodifiableList(Arrays.asList(CLOSED.code, CANCELLED.code));
+
+    /**
+     * 按 code 获取枚举
+     */
+    public static WorkOrderStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (WorkOrderStatusEnum item : values()) {
+            if (Objects.equals(item.code, code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 按 code 获取状态描述,未知返回 "未知"
+     */
+    public static String getDescByCode(Integer code) {
+        WorkOrderStatusEnum item = getByCode(code);
+        return item == null ? "未知" : item.getDesc();
+    }
+
+    /**
+     * 是否为合法状态值
+     */
+    public static boolean isValid(Integer code) {
+        return getByCode(code) != null;
+    }
+
+    /**
+     * 全部状态 code
+     */
+    public static List<Integer> codes() {
+        return Arrays.stream(values()).map(WorkOrderStatusEnum::getCode).collect(Collectors.toList());
+    }
+
+    /** 可取消 */
+    public static boolean canCancel(Integer code) {
+        return code != null && CANCELABLE_CODES.contains(code);
+    }
+
+    /** 可处置(处理) */
+    public static boolean canProcess(Integer code) {
+        return code != null && PROCESSABLE_CODES.contains(code);
+    }
+
+    /** 可提交验收 */
+    public static boolean canSubmitVerify(Integer code) {
+        return code != null && SUBMIT_VERIFY_CODES.contains(code);
+    }
+
+    /** 未办结 */
+    public static boolean isActive(Integer code) {
+        return code != null && ACTIVE_CODES.contains(code);
+    }
+
+    /** 已办结(已结案/已取消) */
+    public static boolean isFinished(Integer code) {
+        return code != null && FINISHED_CODES.contains(code);
+    }
+
+    /** 可取消状态列表 */
+    public static List<Integer> cancelableCodes() {
+        return CANCELABLE_CODES;
+    }
+
+    /** 未办结状态列表 */
+    public static List<Integer> activeCodes() {
+        return ACTIVE_CODES;
+    }
+
+    /** 终态状态列表 */
+    public static List<Integer> finishedCodes() {
+        return FINISHED_CODES;
+    }
+}

+ 63 - 43
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/BusinessWorkOrderService.java

@@ -1,32 +1,42 @@
 package com.zksy.base.service;
 
-import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.WorkOrderLog;
 import com.zksy.base.domain.vo.WorkOrderPushVO;
+import com.zksy.common.core.domain.TreeSelect;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.ops.dto.WorkOrderDelayApplyDTO;
+import com.zksy.ops.dto.WorkOrderDelayAuditDTO;
+import com.zksy.ops.dto.WorkOrderDetailVO;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
+import com.zksy.ops.dto.WorkOrderOperateDTO;
+import com.zksy.ops.dto.WorkOrderPageDTO;
+import com.zksy.ops.dto.WorkOrderVerifyDTO;
 
 import java.util.Date;
 import java.util.List;
 
 /**
- * 业务工单管理 Service(独立于已有 WorkOrderService)
+ * 运维工单 Service(ops 工单流程)
+ *
+ * <p>状态流转:0待派单 →(派单) 1待接单 →(核查) 2已接单 →(处置) 3处理中
+ * →(提交验收) 4待验收 →(验收通过) 5已结案;旁路:6延期中、7已取消</p>
+ *
+ * @author zksy
  */
 public interface BusinessWorkOrderService extends IService<WorkOrder> {
 
     /**
-     * 分页查询工单列表(支持多条件筛选+排序)
-     * @param equipmentType 设备类型:gas-燃气 drainage-排水,为空查询全部
+     * 分页查询工单列表(含数据权限过滤)
      */
-    IPage<WorkOrder> selectWorkOrderList(Integer orderType, Integer orderLevel,
-                                         String orderStatus, String deviceCode,
-                                         String keyword, String equipmentType,
-                                         Integer pageNum, Integer pageSize);
+    Page<WorkOrder> findByPage(WorkOrderPageDTO dto);
 
     /**
-     * 查询工单详情
+     * 查询工单详情(含操作日志)
      */
-    WorkOrder getWorkOrderDetail(Long orderId);
+    WorkOrderDetailVO getDetailById(Long orderId);
 
     /**
      * 查询工单操作日志列表
@@ -34,70 +44,80 @@ public interface BusinessWorkOrderService extends IService<WorkOrder> {
     List<WorkOrderLog> getWorkOrderLogs(Long orderId);
 
     /**
-     * 派单
+     * 新增故障工单(初始状态:待派单)
+     *
+     * @return 工单ID
      */
-    boolean dispatchOrder(Long orderId, Long handlerId, String remarks, Long operUserId, String operUserName);
+    Long addWorkOrder(WorkOrder workOrder);
 
     /**
-     * 批量派单(支持多人)
+     * 派单:待派单 → 待接单
      */
-    boolean batchDispatchOrder(Long orderId, List<Long> handlerIds, String remarks, Long operUserId, String operUserName);
+    boolean dispatch(WorkOrderDispatchDTO dto);
 
     /**
-     * 核查
+     * 核查接单:待接单 → 已接单
      */
-    boolean verifyOrder(Long orderId, String handleResult, String solution, Long operUserId, String operUserName);
+    boolean accept(WorkOrderOperateDTO dto);
 
     /**
-     * 处理
+     * 处置:已接单/处理中/延期中 → 处理中(首次处置同步写入维修前附件)
      */
-    boolean processOrder(Long orderId, String handleResult, String solution, String maintenanceBeforeImg, Long operUserId, String operUserName);
+    boolean process(WorkOrderOperateDTO dto);
 
     /**
-     * 完成/提交验收
+     * 提交验收:处理中/延期中 → 待验收(同步写入维修后附件)
      */
-    boolean completeOrder(Long orderId, String result, String maintenanceAfterImg, Long operUserId, String operUserName);
+    boolean submitVerify(WorkOrderOperateDTO dto);
 
     /**
-     * 取消工单
+     * 验收:待验收 → 已结案(通过) / 处理中(驳回)
      */
-    boolean cancelOrder(Long orderId, Long operUserId, String operUserName);
+    boolean verify(WorkOrderVerifyDTO dto);
 
     /**
-     * 驳回工单(待派单/待接单 -> 已驳回)
-     * @param reason 驳回原因
+     * 取消工单:待派单/待接单/已接单/处理中/待验收 → 已取消
      */
-    boolean rejectOrder(Long orderId, String reason, Long operUserId, String operUserName);
+    boolean cancel(WorkOrderOperateDTO dto);
 
     /**
-     * 验收通过/结案(status 5→6),同步联动隐患状态为"已整改"
+     * 延期申请:处理中且无延期申请 → 待延期审核
      */
-    boolean closeOrder(Long orderId, String result, Long operUserId, String operUserName);
+    boolean applyDelay(WorkOrderDelayApplyDTO dto);
 
     /**
-     * 从隐患创建工单
-     * @param hazardId 隐患ID
-     * @param hazardNo 隐患编号
-     * @param hazardDesc 隐患描述
-     * @param equipmentId 设备ID
-     * @param hazardLevel 隐患等级
-     * @param hazardImages 隐患图片
-     * @param planFinishTime 计划完成时间
-     * @return 工单ID
+     * 延期审核:待延期审核 → 审核通过 / 审核驳回
      */
-    Long createWorkOrderFromHazard(String hazardId, String hazardNo, String hazardDesc, String equipmentId,
-                                    String hazardLevel, String hazardImages, Date planFinishTime);
+    boolean auditDelay(WorkOrderDelayAuditDTO dto);
 
     /**
-     * 手动新增工单(故障维修/日常巡检/设备保养)
-     * @param workOrder 工单信息(orderType/orderLevel/orderDesc/deviceId/deviceCode/planFinishTime)
+     * 删除工单,并级联删除该工单下的操作日志(work_order_log)
+     *
+     * @param orderId 工单ID
+     * @return 是否删除成功
+     */
+    boolean deleteWithLogs(Long orderId);
+
+    /**
+     * 获取可指派部门树
+     */
+    List<TreeSelect> assignableDeptTree();
+
+    /**
+     * 查询可指派用户
+     */
+    List<SysUser> assignableUsers(Long deptId);
+
+    /**
+     * 从隐患创建工单
+     *
      * @return 工单ID
      */
-    Long createWorkOrder(WorkOrder workOrder);
+    Long createWorkOrderFromHazard(String hazardId, String hazardNo, String hazardDesc, String equipmentId,
+                                    String hazardLevel, String hazardImages, Date planFinishTime);
 
     /**
-     * 查询需要通过 WebSocket 提醒的工单。
-     * 状态 1/2 始终纳入;其他未办结状态仅在计划完成时间或延期截止时间超时后纳入。
+     * 查询需要通过 WebSocket 提醒的工单(待派单/待接单 或 已超时)
      */
     List<WorkOrderPushVO> selectPendingOrOverdueWorkOrders(Date now);
 }

+ 5 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/DrillAlarmSyncService.java

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.domain.CaseInfo;
 import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.enums.WorkOrderLogOperTypeEnum;
+import com.zksy.base.enums.WorkOrderStatusEnum;
 import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.WorkOrderLog;
 import com.zksy.base.mapper.CaseInfoMapper;
@@ -48,7 +50,7 @@ public class DrillAlarmSyncService {
         String typeId = equipmentBase.getEquipmentTypeId();
         boolean needCreateCase = CASE_REQUIRED_TYPE_IDS.contains(typeId);
         int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : 4;
-        int orderLevel = caseLevel <= 2 ? 1 : (caseLevel == 3 ? 2 : 3);
+        int orderLevel = caseLevel <= 2 ? 0 : (caseLevel == 3 ? 1 : 2);
         String desc = alarmData.getWarningType() != null ? alarmData.getWarningType() : "设备告警";
         if (alarmData.getRemark() != null) {
             desc += "-" + alarmData.getRemark();
@@ -72,11 +74,10 @@ public class DrillAlarmSyncService {
         workOrder.setAlarmId(alarmData.getId());
         workOrder.setDeviceId(equipmentBase.getEquipmentId());
         workOrder.setDeviceCode(alarmData.getDeviceCode());
-        workOrder.setOrderType(1);
         workOrder.setOrderLevel(orderLevel);
         workOrder.setOrderDesc(desc);
         workOrder.setCreateTime(DateUtil.date());
-        workOrder.setOrderStatus(1);
+        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
         if (caseInfo != null) {
             workOrder.setCaseId(caseInfo.getCaseId());
             workOrder.setCaseNo(caseInfo.getCaseNo());
@@ -85,7 +86,7 @@ public class DrillAlarmSyncService {
         log.info("同步新增工单成功,orderNo={}", workOrder.getOrderNo());
         WorkOrderLog workOrderLog = new WorkOrderLog();
         workOrderLog.setOrderId(workOrder.getOrderId());
-        workOrderLog.setOperType(1);
+        workOrderLog.setOperType(WorkOrderLogOperTypeEnum.CREATE.getCode());
         workOrderLog.setOperDesc(desc + ",系统告警自动创建工单");
         workOrderLog.setOperUserName("系统");
         workOrderLog.setOperUserId(-1L);

+ 589 - 339
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImpl.java

@@ -1,7 +1,7 @@
 package com.zksy.base.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
+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.base.domain.EquipmentBase;
@@ -9,33 +9,86 @@ import com.zksy.base.domain.HazardHiddenAccount;
 import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.WorkOrderLog;
 import com.zksy.base.domain.vo.WorkOrderPushVO;
+import com.zksy.base.enums.WorkOrderDelayApplyStatusEnum;
+import com.zksy.base.enums.WorkOrderLevelEnum;
+import com.zksy.base.enums.WorkOrderLogOperTypeEnum;
+import com.zksy.base.enums.WorkOrderStatusEnum;
 import com.zksy.base.mapper.EquipmentBaseMapper;
 import com.zksy.base.mapper.HazardHiddenAccountMapper;
 import com.zksy.base.mapper.WorkOrderLogMapper;
 import com.zksy.base.mapper.WorkOrderMapper;
 import com.zksy.base.service.BusinessWorkOrderService;
+import com.zksy.common.core.domain.TreeSelect;
 import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.core.domain.model.LoginUser;
+import com.zksy.common.constant.UserConstants;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.DateUtils;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.common.utils.StringUtils;
+import com.zksy.ops.dto.WorkOrderDelayApplyDTO;
+import com.zksy.ops.dto.WorkOrderDelayAuditDTO;
+import com.zksy.ops.dto.WorkOrderDetailVO;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
+import com.zksy.ops.dto.WorkOrderOperateDTO;
+import com.zksy.ops.dto.WorkOrderPageDTO;
+import com.zksy.ops.dto.WorkOrderVerifyDTO;
+import com.zksy.ops.support.OpsAssignScopeSupport;
 import com.zksy.system.service.ISysUserService;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
-import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.Collectors;
 
 /**
- * 业务工单管理 Service 实现
+ * 运维工单 Service 实现(ops 工单流程)
+ *
+ * <p>状态流转:0待派单 →(派单) 1待接单 →(核查) 2已接单 →(处置) 3处理中
+ * →(提交验收) 4待验收 →(验收通过) 5已结案;延期审核通过后进入 6延期中;任意前置状态可 7已取消。</p>
+ *
+ * @author zksy
  */
 @Slf4j
 @Service
 public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder> implements BusinessWorkOrderService {
 
+    /**
+     * 工单编号前缀
+     */
+    private static final String ORDER_NO_PREFIX = "WO";
+
+    /**
+     * 工单编号最大生成重试次数(并发唯一性兜底)
+     */
+    private static final int ORDER_NO_MAX_RETRY = 5;
+
+    /**
+     * 工单编号随机数上限(4位)
+     */
+    private static final int ORDER_NO_RANDOM_BOUND = 10000;
+
+    /**
+     * 框架数据范围参数名,与 DataScopeAspect.DATA_SCOPE 保持一致。
+     * zksy-system 模块不依赖 zksy-framework(DataScopeAspect 所在模块),故此处使用字面量。
+     */
+    private static final String DATA_SCOPE = "dataScope";
+
     @Autowired
     private WorkOrderLogMapper workOrderLogMapper;
 
@@ -48,336 +101,422 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     @Autowired
     private ISysUserService sysUserService;
 
+    @Autowired
+    private OpsAssignScopeSupport assignScopeSupport;
+
     @Override
-    public IPage<WorkOrder> selectWorkOrderList(Integer orderType, Integer orderLevel, String orderStatus, String deviceCode, String keyword, String equipmentType, Integer pageNum, Integer pageSize) {
+    public Page<WorkOrder> findByPage(WorkOrderPageDTO dto) {
+        if (dto == null) {
+            dto = new WorkOrderPageDTO();
+        }
+        long pageNum = (dto.getPageNum() == null || dto.getPageNum() < 1) ? 1L : dto.getPageNum();
+        long pageSize = (dto.getPageSize() == null || dto.getPageSize() < 1) ? 10L : dto.getPageSize();
         Page<WorkOrder> page = new Page<>(pageNum, pageSize);
+
+        if (assignScopeSupport.currentUserId() == null) {
+            // 未获取到登录用户时返回空结果,避免越权查看
+            return new Page<>(pageNum, pageSize);
+        }
+
         LambdaQueryWrapper<WorkOrder> wrapper = new LambdaQueryWrapper<>();
-        // 按设备类型过滤工单:gas-燃气(父类别ID=3),drainage-排水(typeId为drainage的顶级分类),water-供水;为空查询全部设备工单
-        if (StringUtils.isNotEmpty(equipmentType)) {
-            if ("gas".equals(equipmentType)) {
-                wrapper.inSql(WorkOrder::getDeviceId, "SELECT eb.equipment_id FROM app_user.equipment_base eb " + "INNER JOIN app_user.equipment_type et ON eb.equipment_type_id = et.id " + "WHERE et.parent_type_id = '3'");
-            } else if ("drainage".equals(equipmentType)) {
-                // 排水:typeId为drainage的顶级分类及其全部子类型
-                wrapper.inSql(WorkOrder::getDeviceId, "SELECT eb.equipment_id FROM app_user.equipment_base eb " + "INNER JOIN app_user.equipment_type et ON eb.equipment_type_id = et.id " + "WHERE et.id IN (SELECT id FROM app_user.equipment_type WHERE type_id = 'drainage' AND parent_type_id = '0' " + "UNION SELECT id FROM app_user.equipment_type WHERE parent_type_id = (SELECT id FROM app_user.equipment_type WHERE type_id = 'drainage' AND parent_type_id = '0'))");
-            } else if ("water".equals(equipmentType)) {
-                // 供水:顶级类型名称为“供水”的分类及其全部子类型
-                wrapper.inSql(WorkOrder::getDeviceId, "SELECT eb.equipment_id FROM app_user.equipment_base eb " + "INNER JOIN app_user.equipment_type et ON eb.equipment_type_id = et.id " + "WHERE et.id IN (SELECT id FROM app_user.equipment_type WHERE type_name = '供水' AND parent_type_id = '0' " + "UNION SELECT id FROM app_user.equipment_type WHERE parent_type_id = (SELECT id FROM app_user.equipment_type WHERE type_name = '供水' AND parent_type_id = '0'))");
-            }
+        String dataScope = dataScopeSql(dto);
+        if (StringUtils.isNotEmpty(dataScope)) {
+            wrapper.apply(dataScope);
         }
-        if (orderType != null) wrapper.eq(WorkOrder::getOrderType, orderType);
-        if (orderLevel != null) wrapper.eq(WorkOrder::getOrderLevel, orderLevel);
-        // orderStatus 支持逗号分隔多值,如 1,2,3
-        if (StringUtils.isNotEmpty(orderStatus)) {
-            if (orderStatus.contains(",")) {
-                List<Integer> statusList = Arrays.stream(orderStatus.split(","))
-                        .filter(StringUtils::isNumeric)
-                        .map(Integer::valueOf)
-                        .collect(Collectors.toList());
-                if (!statusList.isEmpty()) {
-                    wrapper.in(WorkOrder::getOrderStatus, statusList);
-                }
-            } else {
-                wrapper.eq(WorkOrder::getOrderStatus, Integer.valueOf(orderStatus));
+        wrapper.eq(dto.getOrderLevel() != null, WorkOrder::getOrderLevel, dto.getOrderLevel()).eq(dto.getDelayApplyStatus() != null, WorkOrder::getDelayApplyStatus, dto.getDelayApplyStatus()).eq(StringUtils.isNotEmpty(dto.getDeviceCode()), WorkOrder::getDeviceCode, dto.getDeviceCode()).ge(StringUtils.isNotEmpty(dto.getFaultTimeStart()), WorkOrder::getFaultTime, DateUtils.parseDate(dto.getFaultTimeStart())).le(StringUtils.isNotEmpty(dto.getFaultTimeEnd()), WorkOrder::getFaultTime, DateUtils.parseDate(dto.getFaultTimeEnd()));
+        if (StringUtils.isNotEmpty(dto.getOrderStatus())) {
+            List<Integer> statusList = parseStatusList(dto.getOrderStatus());
+            if (!statusList.isEmpty()) {
+                wrapper.in(WorkOrder::getOrderStatus, statusList);
             }
         }
-        if (StringUtils.isNotEmpty(deviceCode)) wrapper.eq(WorkOrder::getDeviceCode, deviceCode);
-        if (StringUtils.isNotEmpty(keyword)) {
+        if (StringUtils.isNotEmpty(dto.getKeyword())) {
+            String keyword = dto.getKeyword().trim();
             wrapper.and(w -> w.like(WorkOrder::getOrderNo, keyword).or().like(WorkOrder::getDeviceCode, keyword).or().like(WorkOrder::getOrderDesc, keyword));
         }
-        wrapper.orderByAsc(WorkOrder::getOrderLevel, WorkOrder::getOrderStatus);
-        wrapper.orderByDesc(WorkOrder::getCreateTime);
-        return this.page(page, wrapper);
+        applyDeviceTypeScope(wrapper, dto.getDeviceTypeParentId());
+        wrapper.orderByAsc(WorkOrder::getOrderLevel).orderByAsc(WorkOrder::getOrderStatus).orderByDesc(WorkOrder::getFaultTime).orderByDesc(WorkOrder::getCreateTime);
+
+        Page<WorkOrder> result = this.page(page, wrapper);
+        fillUserName(result.getRecords());
+        return result;
     }
 
     @Override
-    public WorkOrder getWorkOrderDetail(Long orderId) {
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        return order;
+    public WorkOrderDetailVO getDetailById(Long orderId) {
+        WorkOrder order = getOrderById(orderId);
+        fillUserName(Collections.singletonList(order));
+        WorkOrderDetailVO vo = new WorkOrderDetailVO();
+        vo.setOrder(order);
+        vo.setLogs(getWorkOrderLogs(orderId));
+        return vo;
     }
 
     @Override
     public List<WorkOrderLog> getWorkOrderLogs(Long orderId) {
-        LambdaQueryWrapper<WorkOrderLog> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(WorkOrderLog::getOrderId, orderId);
-        wrapper.orderByDesc(WorkOrderLog::getOperTime);
-        return workOrderLogMapper.selectList(wrapper);
+        if (orderId == null) {
+            return Collections.emptyList();
+        }
+        List<WorkOrderLog> logs = workOrderLogMapper.selectList(new LambdaQueryWrapper<WorkOrderLog>().eq(WorkOrderLog::getOrderId, orderId).orderByDesc(WorkOrderLog::getOperTime));
+        if (!CollectionUtils.isEmpty(logs)) {
+            for (WorkOrderLog item : logs) {
+                item.setOperTypeDesc(WorkOrderLogOperTypeEnum.getDescByCode(item.getOperType()));
+                item.setAttachList(splitAttach(item.getAttachUrl()));
+            }
+        }
+        return logs;
     }
 
+    // ==================== 新增 ====================
+
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean dispatchOrder(Long orderId, Long handlerId, String remarks, Long operUserId, String operUserName) {
-        validateDispatchHandler(handlerId);
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        if (order.getOrderStatus() != 1) throw new ServiceException("当前工单状态不允许派单");
-
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(2);
-        entity.setDispatchUser(operUserId);
-        entity.setDispatchTime(new Date());
-        entity.setReceiveUser(handlerId.toString());
-        this.updateById(entity);
+    public Long addWorkOrder(WorkOrder workOrder) {
+        if (workOrder == null || StringUtils.isEmpty(workOrder.getOrderDesc())) {
+            throw new ServiceException("故障描述不能为空");
+        }
+        checkDeviceOrderConflict(workOrder.getDeviceId());
+        workOrder.setOrderId(null);
+        workOrder.setOrderNo(generateOrderNo());
+        workOrder.setOrderLevel(WorkOrderLevelEnum.getByCode(workOrder.getOrderLevel()).getCode());
+        if (StringUtils.isNotEmpty(workOrder.getDeviceId()) && StringUtils.isEmpty(workOrder.getDeviceCode())) {
+            workOrder.setDeviceCode(resolveDeviceCode(workOrder.getDeviceId()));
+        }
+        Date now = new Date();
+        if (workOrder.getFaultTime() == null) {
+            workOrder.setFaultTime(now);
+        }
+        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
+        workOrder.setDelayApplyStatus(WorkOrderDelayApplyStatusEnum.NONE.getCode());
+        // 数据按部门隔离:默认归属创建人所在部门(框架 DataScopeAspect 依据该字段过滤)
+        if (workOrder.getDeptId() == null) {
+            workOrder.setDeptId(assignScopeSupport.currentDeptId());
+        }
+        workOrder.setCreateTime(now);
+        workOrder.setUpdateTime(now);
+        this.save(workOrder);
 
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(orderId);
-        logEntry.setOperType(2);
-        logEntry.setOperDesc("派单给处理人ID:" + handlerId + ",备注:" + (remarks != null ? remarks : ""));
-        logEntry.setOperUserId(operUserId);
-        logEntry.setOperUserName(operUserName);
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
-        return true;
+        writeLog(workOrder.getOrderId(), WorkOrderLogOperTypeEnum.CREATE, buildDesc("故障描述", workOrder.getOrderDesc()), null);
+        log.info("手动创建工单成功,工单ID={},编号={}", workOrder.getOrderId(), workOrder.getOrderNo());
+        return workOrder.getOrderId();
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean batchDispatchOrder(Long orderId, List<Long> handlerIds, String remarks, Long operUserId, String operUserName) {
-        if (handlerIds == null || handlerIds.isEmpty()) {
-            throw new ServiceException("派单人员不能为空");
+    public Long createWorkOrderFromHazard(String hazardId, String hazardNo, String hazardDesc, String equipmentId, String hazardLevel, String hazardImages, Date planFinishTime) {
+        if (StringUtils.isEmpty(equipmentId)) {
+            throw new ServiceException("隐患关联设备不能为空");
         }
-        handlerIds.forEach(this::validateDispatchHandler);
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        if (order.getOrderStatus() != 1) throw new ServiceException("当前工单状态不允许派单");
-
-        // 更新工单状态(只更新一次)
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(2);
-        entity.setDispatchUser(operUserId);
-        entity.setDispatchTime(new Date());
-        // 多个处理人用逗号分隔存储
-        String receiveUserStr = handlerIds.stream()
-            .map(String::valueOf)
-            .collect(Collectors.joining(","));
-        entity.setReceiveUser(receiveUserStr);
-        this.updateById(entity);
-
-        // 为每个 handler 写入操作日志
-        for (Long handlerId : handlerIds) {
-            WorkOrderLog logEntry = new WorkOrderLog();
-            logEntry.setOrderId(orderId);
-            logEntry.setOperType(2);
-            logEntry.setOperDesc("派单给处理人ID:" + handlerId + ",备注:" + (remarks != null ? remarks : ""));
-            logEntry.setOperUserId(operUserId);
-            logEntry.setOperUserName(operUserName);
-            logEntry.setOperTime(new Date());
-            workOrderLogMapper.insert(logEntry);
+        checkDeviceOrderConflict(equipmentId);
+
+        WorkOrder workOrder = new WorkOrder();
+        workOrder.setOrderNo(generateOrderNo());
+        workOrder.setHazardId(hazardId);
+        workOrder.setHazardNo(hazardNo);
+        workOrder.setDeviceId(equipmentId);
+        workOrder.setDeviceCode(resolveDeviceCode(equipmentId));
+        workOrder.setOrderLevel(mapHazardLevelToOrderLevel(hazardLevel).getCode());
+        workOrder.setOrderDesc(hazardDesc);
+        // 数据按部门隔离:默认归属创建人所在部门(框架 DataScopeAspect 依据该字段过滤)
+        workOrder.setDeptId(assignScopeSupport.currentDeptId());
+        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
+        workOrder.setDelayApplyStatus(WorkOrderDelayApplyStatusEnum.NONE.getCode());
+        workOrder.setPlanFinishTime(planFinishTime);
+        if (StringUtils.isNotEmpty(hazardImages)) {
+            workOrder.setMaintenanceBeforeImg(hazardImages);
         }
-        return true;
+        Date now = new Date();
+        workOrder.setFaultTime(now);
+        workOrder.setCreateTime(now);
+        workOrder.setUpdateTime(now);
+        this.save(workOrder);
+
+        writeLog(workOrder.getOrderId(), WorkOrderLogOperTypeEnum.CREATE, buildDesc("由隐患[" + hazardNo + "]自动创建工单", hazardDesc), null);
+        log.info("从隐患[{}]创建工单成功,工单ID={}", hazardId, workOrder.getOrderId());
+        return workOrder.getOrderId();
     }
 
-    private void validateDispatchHandler(Long handlerId) {
+    // ==================== 流程操作 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean dispatch(WorkOrderDispatchDTO dto) {
+        Long orderId = requireOrderId(dto == null ? null : dto.getOrderId());
+        Long handlerId = dto.getHandlerId();
         if (handlerId == null) {
-            throw new ServiceException("派单人员不能为空");
+            throw new ServiceException("责任人不能为空");
         }
-        if (sysUserService == null) {
-            throw new ServiceException("责任人校验服务不可用");
+        WorkOrder order = getOrderById(orderId);
+        if (!Objects.equals(WorkOrderStatusEnum.PENDING_DISPATCH.getCode(), order.getOrderStatus())) {
+            throw new ServiceException("仅待派单工单允许派单");
         }
-        SysUser user = sysUserService.selectUserById(handlerId);
-        if (user == null || !"0".equals(String.valueOf(user.getStatus()))
-                || !"0".equals(String.valueOf(user.getDelFlag()))) {
-            throw new ServiceException("接单责任人不存在或已停用");
+        assignScopeSupport.validateAssignUserInScope(handlerId);
+        SysUser handler = sysUserService.selectUserById(handlerId);
+        if (handler == null || !UserConstants.NORMAL.equals(String.valueOf(handler.getStatus())) || !UserConstants.NORMAL.equals(String.valueOf(handler.getDelFlag()))) {
+            throw new ServiceException("责任人不存在或已停用");
         }
+
+        Long operUserId = currentUserId();
+        Date now = new Date();
+        this.update(new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_ACCEPT.getCode()).set(WorkOrder::getReceiveUser, handlerId).set(WorkOrder::getDispatchUser, operUserId).set(WorkOrder::getDispatchTime, now)
+                // 数据归属部门跟随派单指定的责任人
+                .set(handler.getDeptId() != null, WorkOrder::getDeptId, handler.getDeptId()).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, orderId).eq(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_DISPATCH.getCode()));
+
+        String logContent = "派单给责任人:" + userLabel(handler) + (StringUtils.isEmpty(dto.getRemarks()) ? "" : ",备注:" + dto.getRemarks());
+        writeLog(orderId, WorkOrderLogOperTypeEnum.DISPATCH, logContent, null);
+        return true;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean verifyOrder(Long orderId, String handleResult, String solution, Long operUserId, String operUserName) {
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        if (!Arrays.asList(2, 3).contains(order.getOrderStatus())) throw new ServiceException("当前工单状态不允许核查");
-
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(3);
-        entity.setRepairResult(solution);
-        this.updateById(entity);
+    public boolean accept(WorkOrderOperateDTO dto) {
+        WorkOrder order = loadForOperate(dto, "核查接单");
+        if (!Objects.equals(WorkOrderStatusEnum.PENDING_ACCEPT.getCode(), order.getOrderStatus())) {
+            throw new ServiceException("仅待接单工单允许核查接单");
+        }
+        // 核查接单必须上传现场情况附件
+        if (StringUtils.isEmpty(attach(dto))) {
+            throw new ServiceException("核查接单必须上传现场情况附件");
+        }
+        Date now = new Date();
+        LambdaUpdateWrapper<WorkOrder> uw = new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getOrderStatus, WorkOrderStatusEnum.ACCEPTED.getCode())
+                // 维修前图片固定来源于核查接单上传的现场情况附件
+                .set(WorkOrder::getMaintenanceBeforeImg, attach(dto)).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, order.getOrderId()).eq(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_ACCEPT.getCode());
+        this.update(uw);
 
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(orderId);
-        logEntry.setOperType(3);
-        logEntry.setOperDesc("核查结果:" + handleResult + ",说明:" + solution);
-        logEntry.setOperUserId(operUserId);
-        logEntry.setOperUserName(operUserName);
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
+        writeLog(order.getOrderId(), WorkOrderLogOperTypeEnum.ACCEPT, buildDesc("", content(dto)), attach(dto));
         return true;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean processOrder(Long orderId, String handleResult, String solution, String maintenanceBeforeImg, Long operUserId, String operUserName) {
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        if (!Arrays.asList(3, 4).contains(order.getOrderStatus())) throw new ServiceException("当前工单状态不允许处理");
-
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(4);
-        entity.setRepairResult(solution);
-        if (StringUtils.isNotBlank(maintenanceBeforeImg)) {
-            entity.setMaintenanceBeforeImg(maintenanceBeforeImg);
+    public boolean process(WorkOrderOperateDTO dto) {
+        WorkOrder order = loadForOperate(dto, "处置");
+        Integer status = order.getOrderStatus();
+        if (!WorkOrderStatusEnum.canProcess(status)) {
+            throw new ServiceException("当前工单状态不允许处置");
         }
-        this.updateById(entity);
+        Date now = new Date();
+        // 已接单 → 处理中;处理中/延期中 保持原状态,支持多次处置
+        Integer targetStatus = Objects.equals(WorkOrderStatusEnum.ACCEPTED.getCode(), status) ? WorkOrderStatusEnum.PROCESSING.getCode() : status;
+        LambdaUpdateWrapper<WorkOrder> uw = new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getOrderStatus, targetStatus).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, order.getOrderId()).eq(WorkOrder::getOrderStatus, status);
+        this.update(uw);
+
+        syncHazardStatus(order, "整改中", content(dto));
+        writeLog(order.getOrderId(), WorkOrderLogOperTypeEnum.PROCESS, buildDesc("", content(dto)), attach(dto));
+        return true;
+    }
 
-        // 工单处理中 → 同步隐患状态为"整改中"
-        syncHazardStatus(order, "整改中", solution);
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean submitVerify(WorkOrderOperateDTO dto) {
+        WorkOrder order = loadForOperate(dto, "提交验收");
+        Integer status = order.getOrderStatus();
+        if (!WorkOrderStatusEnum.canSubmitVerify(status)) {
+            throw new ServiceException("仅处理中/延期中工单允许提交验收");
+        }
+        // 提交验收必须上传维修后的情况附件
+        if (StringUtils.isEmpty(attach(dto))) {
+            throw new ServiceException("提交验收必须上传维修后的情况附件");
+        }
+        Date now = new Date();
+        // 提交验收:附件已在上方强制必填,直接写入工单维修后图片
+        LambdaUpdateWrapper<WorkOrder> uw = new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_VERIFY.getCode()).set(WorkOrder::getMaintenanceAfterImg, attach(dto)).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, order.getOrderId()).eq(WorkOrder::getOrderStatus, status);
+        this.update(uw);
 
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(orderId);
-        logEntry.setOperType(4);
-        logEntry.setOperDesc("处理结果:" + handleResult + ",说明:" + solution);
-        logEntry.setOperUserId(operUserId);
-        logEntry.setOperUserName(operUserName);
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
+        writeLog(order.getOrderId(), WorkOrderLogOperTypeEnum.SUBMIT_VERIFY, buildDesc("", content(dto)), attach(dto));
         return true;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean completeOrder(Long orderId, String result, String maintenanceAfterImg, Long operUserId, String operUserName) {
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        if (order.getOrderStatus() != 4)
-            throw new ServiceException("当前工单状态不允许提交验收");
-
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(5);
-        entity.setRepairResult(result);
-        if (StringUtils.isNotBlank(maintenanceAfterImg)) {
-            entity.setMaintenanceAfterImg(maintenanceAfterImg);
+    public boolean verify(WorkOrderVerifyDTO dto) {
+        Long orderId = requireOrderId(dto == null ? null : dto.getOrderId());
+        Boolean passed = dto == null ? null : dto.getPassed();
+        if (passed == null) {
+            throw new ServiceException("验收结果不能为空");
         }
-        this.updateById(entity);
+        WorkOrder order = getOrderById(orderId);
+        if (!Objects.equals(WorkOrderStatusEnum.PENDING_VERIFY.getCode(), order.getOrderStatus())) {
+            throw new ServiceException("仅待验收工单允许验收");
+        }
+        Long operUserId = currentUserId();
+        Date now = new Date();
+        WorkOrderStatusEnum targetStatus = passed ? WorkOrderStatusEnum.CLOSED : WorkOrderStatusEnum.PROCESSING;
+        LambdaUpdateWrapper<WorkOrder> uw = new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getOrderStatus, targetStatus.getCode()).set(WorkOrder::getVerifyUser, operUserId).set(WorkOrder::getVerifyTime, now).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, orderId).eq(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_VERIFY.getCode());
+        if (passed) {
+            uw.set(WorkOrder::getRealFinishTime, now);
+        }
+        if (StringUtils.isNotEmpty(dto.getContent())) {
+            uw.set(WorkOrder::getRepairResult, dto.getContent());
+        }
+        this.update(uw);
 
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(orderId);
-        logEntry.setOperType(5);
-        logEntry.setOperDesc("提交验收,结果:" + result);
-        logEntry.setOperUserId(operUserId);
-        logEntry.setOperUserName(operUserName);
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
+        if (passed) {
+            syncHazardStatus(order, "已整改", dto.getContent());
+        }
+        writeLog(orderId, passed ? WorkOrderLogOperTypeEnum.VERIFY_PASS : WorkOrderLogOperTypeEnum.VERIFY_REJECT, buildDesc("验收意见", dto.getContent()), dto.getAttachUrl());
         return true;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Long createWorkOrderFromHazard(String hazardId, String hazardNo, String hazardDesc, String equipmentId, String hazardLevel, String hazardImages, Date planFinishTime) {
-        // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
-        checkDeviceOrderConflict(equipmentId, 1, null);
-
-        // 隐患等级映射到工单优先级:0-重大→紧急(1) 1-较大→一般(2) 2-一般→一般(2) 3-轻微→低(3)
-        Integer orderLevel;
-        if ("0".equals(hazardLevel)) {
-            orderLevel = 1;
-        } else if ("3".equals(hazardLevel)) {
-            orderLevel = 3;
-        } else {
-            orderLevel = 2;
+    public boolean cancel(WorkOrderOperateDTO dto) {
+        Long orderId = requireOrderId(dto == null ? null : dto.getOrderId());
+        WorkOrder order = getOrderById(orderId);
+        if (!WorkOrderStatusEnum.canCancel(order.getOrderStatus())) {
+            throw new ServiceException("当前工单状态不允许取消");
+        }
+        if (StringUtils.isEmpty(content(dto))) {
+            throw new ServiceException("请填写取消原因");
         }
+        Date now = new Date();
+        this.update(new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getOrderStatus, WorkOrderStatusEnum.CANCELLED.getCode()).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, orderId).eq(WorkOrder::getOrderStatus, order.getOrderStatus()));
 
-        WorkOrder workOrder = new WorkOrder();
-        workOrder.setOrderNo(generateOrderNo());
-        workOrder.setHazardId(hazardId);
-        workOrder.setHazardNo(hazardNo);
-        workOrder.setDeviceId(equipmentId);
-        // 查询设备信息,填充设备编码
-        if (StringUtils.isNotEmpty(equipmentId)) {
-            EquipmentBase equipment = equipmentBaseMapper.selectById(equipmentId);
-            if (equipment != null) {
-                workOrder.setDeviceCode(equipment.getEquipmentCode());
-            }
+        writeLog(orderId, WorkOrderLogOperTypeEnum.CANCEL, buildDesc("原因", content(dto)), attach(dto));
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean applyDelay(WorkOrderDelayApplyDTO dto) {
+        Long orderId = requireOrderId(dto == null ? null : dto.getOrderId());
+        WorkOrder order = getOrderById(orderId);
+        Long currentUserId = currentUserId();
+        requireResponsible(order, currentUserId, "申请延期");
+        if (!Objects.equals(WorkOrderStatusEnum.PROCESSING.getCode(), order.getOrderStatus())) {
+            throw new ServiceException("仅处理中工单允许申请延期");
         }
-        workOrder.setOrderType(1); // 默认故障维修
-        workOrder.setOrderLevel(orderLevel);
-        workOrder.setOrderDesc(hazardDesc);
-        workOrder.setOrderStatus(1); // 待派单
-        // 隐患图片作为维修前图片(如有)
-        if (StringUtils.isNotEmpty(hazardImages)) {
-            workOrder.setMaintenanceBeforeImg(hazardImages);
+        if (!Objects.equals(WorkOrderDelayApplyStatusEnum.NONE.getCode(), order.getDelayApplyStatus())) {
+            throw new ServiceException("该工单已提交过延期申请,请勿重复提交");
+        }
+        if (StringUtils.isEmpty(dto.getDelayedReason())) {
+            throw new ServiceException("延期原因不能为空");
+        }
+        Date delayedDeadline = DateUtils.parseDate(dto.getDelayedDeadline());
+        if (delayedDeadline == null) {
+            throw new ServiceException("延期截止时间格式不正确");
         }
-        workOrder.setPlanFinishTime(planFinishTime);
-        workOrder.setCreateTime(new Date());
-        this.save(workOrder);
 
-        // 写入操作日志
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(workOrder.getOrderId());
-        logEntry.setOperType(1);
-        logEntry.setOperDesc("由隐患[" + hazardId + "]自动创建工单");
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
+        Date now = new Date();
+        this.update(new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getDelayApplyStatus, WorkOrderDelayApplyStatusEnum.PENDING_AUDIT.getCode()).set(WorkOrder::getDelayedDeadline, delayedDeadline).set(WorkOrder::getDelayedReason, dto.getDelayedReason()).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, orderId).eq(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PROCESSING.getCode()).eq(WorkOrder::getDelayApplyStatus, WorkOrderDelayApplyStatusEnum.NONE.getCode()));
 
-        log.info("从隐患[{}]创建工单成功,工单ID={}", hazardId, workOrder.getOrderId());
-        return workOrder.getOrderId();
+        writeLog(orderId, WorkOrderLogOperTypeEnum.DELAY_APPLY, "申请延期至 " + dto.getDelayedDeadline() + ",原因:" + dto.getDelayedReason(), dto.getAttachUrl());
+        return true;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Long createWorkOrder(WorkOrder workOrder) {
-        // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
-        if (workOrder.getOrderType() == null) {
-            workOrder.setOrderType(1);
+    public boolean auditDelay(WorkOrderDelayAuditDTO dto) {
+        Long orderId = requireOrderId(dto == null ? null : dto.getOrderId());
+        Boolean approved = dto.getApproved();
+        if (approved == null) {
+            throw new ServiceException("延期审核结果不能为空");
         }
-        checkDeviceOrderConflict(workOrder.getDeviceId(), workOrder.getOrderType(), workOrder.getAlarmId());
-        if (workOrder.getOrderLevel() == null) {
-            workOrder.setOrderLevel(2);
+        WorkOrder order = getOrderById(orderId);
+        if (!Objects.equals(WorkOrderDelayApplyStatusEnum.PENDING_AUDIT.getCode(), order.getDelayApplyStatus())) {
+            throw new ServiceException("该工单没有待审核的延期申请");
         }
-        workOrder.setOrderNo(generateOrderNo());
-        // 设备ID存在但未填设备编码时自动补全
-        if (StringUtils.isNotEmpty(workOrder.getDeviceId()) && StringUtils.isEmpty(workOrder.getDeviceCode())) {
-            EquipmentBase equipment = equipmentBaseMapper.selectById(workOrder.getDeviceId());
-            if (equipment != null) {
-                workOrder.setDeviceCode(equipment.getEquipmentCode());
-            }
+        WorkOrderDelayApplyStatusEnum applyStatus = approved ? WorkOrderDelayApplyStatusEnum.APPROVED : WorkOrderDelayApplyStatusEnum.REJECTED;
+        Integer orderStatus = approved ? WorkOrderStatusEnum.DELAYING.getCode() : order.getOrderStatus();
+        Date now = new Date();
+        this.update(new LambdaUpdateWrapper<WorkOrder>().set(WorkOrder::getDelayApplyStatus, applyStatus.getCode()).set(WorkOrder::getOrderStatus, orderStatus).set(WorkOrder::getUpdateTime, now).eq(WorkOrder::getOrderId, orderId).eq(WorkOrder::getDelayApplyStatus, WorkOrderDelayApplyStatusEnum.PENDING_AUDIT.getCode()));
+
+        writeLog(orderId, approved ? WorkOrderLogOperTypeEnum.DELAY_APPROVE : WorkOrderLogOperTypeEnum.DELAY_REJECT, buildDesc("审核意见", dto.getAuditOpinion()), null);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteWithLogs(Long orderId) {
+        if (orderId == null) {
+            return false;
         }
-        workOrder.setOrderStatus(1); // 待派单
-        workOrder.setCreateTime(new Date());
-        this.save(workOrder);
+        // 先清理该工单的操作日志,避免 work_order_log 残留脏数据
+        workOrderLogMapper.delete(new LambdaQueryWrapper<WorkOrderLog>().eq(WorkOrderLog::getOrderId, orderId));
+        return this.removeById(orderId);
+    }
 
-        // 写入操作日志
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(workOrder.getOrderId());
-        logEntry.setOperType(1);
-        logEntry.setOperDesc("手动创建工单");
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
+    private String dataScopeSql(WorkOrderPageDTO dto) {
+        Map<String, Object> params = dto.getParams();
+        if (params == null || params.isEmpty()) {
+            return null;
+        }
+        Object dataScope = params.get(DATA_SCOPE);
+        if (dataScope == null) {
+            return null;
+        }
+        String sql = String.valueOf(dataScope).trim();
+        if (sql.length() == 0) {
+            return null;
+        }
+        String upper = sql.toUpperCase(Locale.ROOT);
+        if (upper.startsWith("AND ")) {
+            sql = sql.substring(4).trim();
+        } else if (upper.startsWith("OR ")) {
+            sql = sql.substring(3).trim();
+        }
+        return sql.length() == 0 ? null : sql;
+    }
 
-        log.info("手动创建工单成功,工单ID={},类型={}", workOrder.getOrderId(), workOrder.getOrderType());
-        return workOrder.getOrderId();
+    /**
+     * 按设备父类别收窄工单查询范围(业务口径过滤)。
+     *
+     * <p>过滤链路:work_order.device_id → equipment_base.equipment_id
+     * → equipment_base.equipment_type_id → equipment_type.id → equipment_type.parent_type_id。
+     * 采用 EXISTS 子查询而非 JOIN,避免设备表一对多时放大分页结果集,也不影响已有的
+     * 数据权限片段(dataScope)与 MyBatis-Plus 分页计数。
+     *
+     * <p>parentTypeId 通过 MyBatis-Plus 的 apply 占位符 {0} 注入,由框架做 SQL 注入检查,不拼接原始入参。
+     *
+     * @param wrapper       工单查询条件
+     * @param parentTypeId  设备父类别ID,为空表示不限制
+     */
+    private void applyDeviceTypeScope(LambdaQueryWrapper<WorkOrder> wrapper, String parentTypeId) {
+        if (StringUtils.isEmpty(parentTypeId)) {
+            return;
+        }
+        wrapper.apply("EXISTS (SELECT 1 FROM app_user.equipment_base eb "
+                        + "INNER JOIN app_user.equipment_type et ON et.id = eb.equipment_type_id "
+                        + "WHERE eb.equipment_id = app_user.work_order.device_id "
+                        + "AND et.parent_type_id = {0})",
+                parentTypeId.trim());
+    }
+
+    @Override
+    public List<TreeSelect> assignableDeptTree() {
+        return assignScopeSupport.assignableDeptTree();
+    }
+
+    @Override
+    public List<SysUser> assignableUsers(Long deptId) {
+        return assignScopeSupport.assignableUsers(deptId);
     }
 
     @Override
     public List<WorkOrderPushVO> selectPendingOrOverdueWorkOrders(Date now) {
         Date currentTime = now != null ? now : new Date();
         List<WorkOrder> orders = baseMapper.selectPendingOrOverdue(currentTime);
-
         List<WorkOrderPushVO> result = new ArrayList<>();
         if (orders == null || orders.isEmpty()) {
             return result;
         }
         for (WorkOrder order : orders) {
             Integer status = order.getOrderStatus();
-            Date effectiveDeadline = order.getDelayedDeadline() != null
-                    ? order.getDelayedDeadline() : order.getPlanFinishTime();
-            boolean pending = Integer.valueOf(1).equals(status) || Integer.valueOf(2).equals(status);
+            Date effectiveDeadline = order.getDelayedDeadline() != null ? order.getDelayedDeadline() : order.getPlanFinishTime();
+            boolean pending = Objects.equals(WorkOrderStatusEnum.PENDING_DISPATCH.getCode(), status) || Objects.equals(WorkOrderStatusEnum.PENDING_ACCEPT.getCode(), status);
             boolean overdue = effectiveDeadline != null && effectiveDeadline.before(currentTime);
             if (!pending && !overdue) {
                 continue;
             }
-
             WorkOrderPushVO item = new WorkOrderPushVO();
             item.setOrderId(order.getOrderId());
             item.setOrderNo(order.getOrderNo());
             item.setOrderStatus(status);
+            item.setOrderStatusDesc(WorkOrderStatusEnum.getDescByCode(status));
             item.setOrderLevel(order.getOrderLevel());
             item.setDeptId(order.getDeptId());
-            item.setReceiveUser(order.getReceiveUser());
+            item.setReceiveUser(order.getReceiveUser() == null ? null : String.valueOf(order.getReceiveUser()));
             item.setDeviceId(order.getDeviceId());
             item.setDeviceCode(order.getDeviceCode());
             item.setOrderDesc(order.getOrderDesc());
@@ -385,11 +524,13 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
             item.setDelayedDeadline(order.getDelayedDeadline());
             item.setEffectiveDeadline(effectiveDeadline);
             item.setOverdue(overdue);
-            item.setOverdueType(overdue
-                    ? order.getDelayedDeadline() != null ? "DELAYED_DEADLINE" : "PLAN_FINISH_TIME"
-                    : "PENDING");
-            item.setOverdueMinutes(overdue
-                    ? Math.max(0L, (currentTime.getTime() - effectiveDeadline.getTime()) / 60000L) : 0L);
+            if (overdue) {
+                item.setOverdueType(order.getDelayedDeadline() != null ? "DELAYED_DEADLINE" : "PLAN_FINISH_TIME");
+                item.setOverdueMinutes(Math.max(0L, (currentTime.getTime() - effectiveDeadline.getTime()) / 60000L));
+            } else {
+                item.setOverdueType("PENDING");
+                item.setOverdueMinutes(0L);
+            }
             item.setCreateTime(order.getCreateTime());
             item.setUpdateTime(order.getUpdateTime());
             result.add(item);
@@ -397,129 +538,126 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         return result;
     }
 
+    // ==================== 私有方法 ====================
+
     /**
-     * 校验同一业务对象是否已经存在进行中的同类工单。
-     * 带报警身份的工单按设备、工单类型和报警记录去重;没有报警身份的
-     * 手工工单只与同设备的另一条未关联报警工单冲突,避免挡住不同报警原因。
+     * 加载工单并校验操作人是责任人本人(超级管理员放行)
      */
-    private void checkDeviceOrderConflict(String deviceId, Integer orderType, String alarmId) {
-        if (StringUtils.isEmpty(deviceId)) {
-            return;
-        }
-        com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper<WorkOrder> query = this.lambdaQuery()
-                .eq(WorkOrder::getDeviceId, deviceId)
-                .notIn(WorkOrder::getOrderStatus, 6, 7);
-        if (orderType != null) {
-            query.eq(WorkOrder::getOrderType, orderType);
-        }
-        if (StringUtils.isNotBlank(alarmId)) {
-            query.eq(WorkOrder::getAlarmId, alarmId);
-        } else {
-            query.isNull(WorkOrder::getAlarmId);
-        }
-        Long count = query.count();
-        if (count > 0) {
-            throw new ServiceException("该设备已有处理中的工单,需结案或驳回后才能创建新工单");
-        }
+    private WorkOrder loadForOperate(WorkOrderOperateDTO dto, String actionName) {
+        Long orderId = requireOrderId(dto == null ? null : dto.getOrderId());
+        WorkOrder order = getOrderById(orderId);
+        requireResponsible(order, currentUserId(), actionName);
+        return order;
     }
 
     /**
-     * 生成工单编号:WO-yyyyMMddHHmmssSSS
+     * 校验操作人是工单责任人本人,超级管理员放行
      */
-    private String generateOrderNo() {
-        return "WO-" + new java.text.SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
+    private void requireResponsible(WorkOrder order, Long operUserId, String actionName) {
+        if (operUserId == null) {
+            throw new ServiceException("未获取到登录用户,无法" + actionName);
+        }
+        if (SecurityUtils.isAdmin(operUserId)) {
+            return;
+        }
+        if (order.getReceiveUser() == null || !Objects.equals(order.getReceiveUser(), operUserId)) {
+            throw new ServiceException("仅工单责任人可" + actionName);
+        }
     }
 
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public boolean cancelOrder(Long orderId, Long operUserId, String operUserName) {
+    private WorkOrder getOrderById(Long orderId) {
+        if (orderId == null) {
+            throw new ServiceException("工单ID不能为空");
+        }
         WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        if (order.getOrderStatus() == 6) throw new ServiceException("已结案工单不可取消");
-
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(7);
-        this.updateById(entity);
-
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(orderId);
-        logEntry.setOperType(7);
-        logEntry.setOperDesc("取消工单");
-        logEntry.setOperUserId(operUserId);
-        logEntry.setOperUserName(operUserName);
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
-        return true;
+        if (order == null) {
+            throw new ServiceException("工单不存在或已被删除");
+        }
+        return order;
     }
 
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public boolean rejectOrder(Long orderId, String reason, Long operUserId, String operUserName) {
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        // 仅待派单/待接单可驳回,已流转的工单请使用取消
-        if (!Arrays.asList(1, 2).contains(order.getOrderStatus())) throw new ServiceException("当前工单状态不允许驳回");
-
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(7);
-        this.updateById(entity);
+    private Long requireOrderId(Long orderId) {
+        if (orderId == null) {
+            throw new ServiceException("工单ID不能为空");
+        }
+        return orderId;
+    }
 
+    /**
+     * 写入工单操作日志(操作人取当前登录用户)
+     */
+    private void writeLog(Long orderId, WorkOrderLogOperTypeEnum operType, String operDesc, String attachUrl) {
         WorkOrderLog logEntry = new WorkOrderLog();
         logEntry.setOrderId(orderId);
-        logEntry.setOperType(7);
-        logEntry.setOperDesc("驳回工单,原因:" + (reason != null ? reason : ""));
-        logEntry.setOperUserId(operUserId);
-        logEntry.setOperUserName(operUserName);
+        logEntry.setOperType(operType.getCode());
+        logEntry.setOperDesc(operDesc);
+        logEntry.setAttachUrl(attachUrl);
+        logEntry.setOperUserId(currentUserId());
+        logEntry.setOperUserName(currentUserName());
         logEntry.setOperTime(new Date());
         workOrderLogMapper.insert(logEntry);
-        return true;
     }
 
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public boolean closeOrder(Long orderId, String result, Long operUserId, String operUserName) {
-        WorkOrder order = this.getById(orderId);
-        if (order == null) throw new ServiceException("工单不存在");
-        if (order.getOrderStatus() != 5) throw new ServiceException("当前工单状态不允许验收通过");
-
-        WorkOrder entity = new WorkOrder();
-        entity.setOrderId(orderId);
-        entity.setOrderStatus(6);
-        entity.setRealFinishTime(new Date());
-        this.updateById(entity);
+    /**
+     * 校验同一设备是否已存在未办结工单
+     */
+    private void checkDeviceOrderConflict(String deviceId) {
+        if (StringUtils.isEmpty(deviceId)) {
+            return;
+        }
+        Long count = this.lambdaQuery().eq(WorkOrder::getDeviceId, deviceId).in(WorkOrder::getOrderStatus, WorkOrderStatusEnum.activeCodes()).count();
+        if (count != null && count > 0) {
+            throw new ServiceException("该设备已有处理中的工单,需结案或取消后才能创建新工单");
+        }
+    }
 
-        // 工单已结案 → 同步隐患状态为"已整改"
-        syncHazardStatus(order, "已整改", result);
+    /**
+     * 生成全局唯一工单编号:WO + 14位时间戳 + 4位随机数
+     */
+    private String generateOrderNo() {
+        String pattern = "yyyyMMddHHmmss";
+        for (int i = 0; i < ORDER_NO_MAX_RETRY; i++) {
+            String no = ORDER_NO_PREFIX + new SimpleDateFormat(pattern).format(new Date()) + String.format("%04d", ThreadLocalRandom.current().nextInt(ORDER_NO_RANDOM_BOUND));
+            Long count = this.count(new LambdaQueryWrapper<WorkOrder>().eq(WorkOrder::getOrderNo, no));
+            if (count == null || count == 0) {
+                return no;
+            }
+        }
+        throw new ServiceException("工单编号生成失败,请重试");
+    }
 
-        // 设备报警状态由实时监测链路维护;工单结案不直接清除报警,避免覆盖仍存在的实时告警。
+    /**
+     * 隐患等级映射工单优先级:0-重大→紧急 3-轻微→低 其余→一般
+     */
+    private WorkOrderLevelEnum mapHazardLevelToOrderLevel(String hazardLevel) {
+        if ("0".equals(hazardLevel)) {
+            return WorkOrderLevelEnum.URGENT;
+        }
+        if ("3".equals(hazardLevel)) {
+            return WorkOrderLevelEnum.LOW;
+        }
+        return WorkOrderLevelEnum.NORMAL;
+    }
 
-        WorkOrderLog logEntry = new WorkOrderLog();
-        logEntry.setOrderId(orderId);
-        logEntry.setOperType(6);
-        logEntry.setOperDesc("验收通过,结果:" + result);
-        logEntry.setOperUserId(operUserId);
-        logEntry.setOperUserName(operUserName);
-        logEntry.setOperTime(new Date());
-        workOrderLogMapper.insert(logEntry);
-        return true;
+    private String resolveDeviceCode(String deviceId) {
+        try {
+            EquipmentBase equipment = equipmentBaseMapper.selectById(deviceId);
+            return equipment == null ? null : equipment.getEquipmentCode();
+        } catch (Exception e) {
+            log.warn("查询设备编码失败,deviceId={}", deviceId, e);
+            return null;
+        }
     }
 
     /**
      * 同步隐患状态:根据工单处理结果更新关联隐患记录的状态和整改反馈
-     * @param order 工单对象
-     * @param hazardStatus 隐患状态:整改中/已整改
-     * @param rectifyFeedback 整改反馈(说明/方案)
      */
     private void syncHazardStatus(WorkOrder order, String hazardStatus, String rectifyFeedback) {
         if (StringUtils.isEmpty(order.getHazardNo())) {
             return;
         }
         try {
-            LambdaQueryWrapper<HazardHiddenAccount> wrapper = new LambdaQueryWrapper<>();
-            wrapper.eq(HazardHiddenAccount::getHazardNo, order.getHazardNo());
-            HazardHiddenAccount hazard = hazardHiddenAccountMapper.selectOne(wrapper);
+            HazardHiddenAccount hazard = hazardHiddenAccountMapper.selectOne(new LambdaQueryWrapper<HazardHiddenAccount>().eq(HazardHiddenAccount::getHazardNo, order.getHazardNo()));
             if (hazard != null) {
                 HazardHiddenAccount update = new HazardHiddenAccount();
                 update.setHazardId(hazard.getHazardId());
@@ -528,12 +666,124 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
                     update.setRectifyFeedback(rectifyFeedback);
                 }
                 hazardHiddenAccountMapper.updateById(update);
-                log.info("工单[{}]关联隐患[{}]状态同步为:{}, 整改反馈:{}",
-                        order.getOrderNo(), order.getHazardNo(), hazardStatus, rectifyFeedback);
+                log.info("工单[{}]关联隐患[{}]状态同步为:{}", order.getOrderNo(), order.getHazardNo(), hazardStatus);
             }
         } catch (Exception e) {
             log.error("工单[{}]同步隐患状态失败", order.getOrderNo(), e);
         }
     }
 
+    /**
+     * 批量回填责任人/派单人姓名,避免逐条查库
+     */
+    private void fillUserName(List<WorkOrder> records) {
+        if (CollectionUtils.isEmpty(records)) {
+            return;
+        }
+        Map<Long, String> userMap = loadUserMap(records);
+        for (WorkOrder record : records) {
+            if (record.getReceiveUser() != null) {
+                record.setReceiveUserName(userMap.getOrDefault(record.getReceiveUser(), String.valueOf(record.getReceiveUser())));
+            }
+            if (record.getDispatchUser() != null) {
+                record.setDispatchUserName(userMap.getOrDefault(record.getDispatchUser(), String.valueOf(record.getDispatchUser())));
+            }
+        }
+    }
+
+    /**
+     * 按本页实际涉及的用户ID批量查询并构建「用户ID → 姓名」映射。
+     *
+     * <p>此前是全量拉取用户表(selectAllEnabledUser)再建 Map,用户量大时每次分页查询都要扫描全表。
+     * 实际只用到当页记录的 receiveUser / dispatchUser,故改为按 ID 精确查询。
+     *
+     * @param records 当前页工单记录
+     * @return 用户ID → 姓名的映射;查询失败时返回空 Map(降级为展示ID,不影响主流程)
+     */
+    private Map<Long, String> loadUserMap(List<WorkOrder> records) {
+        Set<Long> userIds = new HashSet<>();
+        for (WorkOrder record : records) {
+            if (record.getReceiveUser() != null) {
+                userIds.add(record.getReceiveUser());
+            }
+            if (record.getDispatchUser() != null) {
+                userIds.add(record.getDispatchUser());
+            }
+        }
+        if (userIds.isEmpty()) {
+            return new HashMap<>(0);
+        }
+        try {
+            List<SysUser> users = sysUserService.selectEnabledUsersByIds(userIds);
+            if (CollectionUtils.isEmpty(users)) {
+                return new HashMap<>(0);
+            }
+            Map<Long, String> userMap = new HashMap<>(users.size() * 2);
+            for (SysUser user : users) {
+                if (user.getUserId() != null) {
+                    userMap.put(user.getUserId(), userLabel(user));
+                }
+            }
+            return userMap;
+        } catch (Exception e) {
+            log.warn("加载工单责任人用户映射失败", e);
+            return new HashMap<>(0);
+        }
+    }
+
+    private String userLabel(SysUser user) {
+        if (user == null) {
+            return "";
+        }
+        return StringUtils.isNotEmpty(user.getNickName()) ? user.getNickName() : user.getUserName();
+    }
+
+    private Long currentUserId() {
+        return assignScopeSupport.currentUserId();
+    }
+
+    /**
+     * 获取当前登录用户姓名:用户昵称(nickName)优先,缺省时回退到登录账号。
+     * 工单操作日志展示姓名比账号更直观,与 WaterSupplyAlarmServiceImpl 保持一致。
+     */
+    private String currentUserName() {
+        try {
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+            if (loginUser == null) {
+                return null;
+            }
+            if (loginUser.getUser() != null && StringUtils.isNotEmpty(loginUser.getUser().getNickName())) {
+                return loginUser.getUser().getNickName();
+            }
+            return loginUser.getUsername();
+        } catch (Exception e) {
+            log.warn("获取当前登录用户姓名失败", e);
+            return null;
+        }
+    }
+
+    private List<Integer> parseStatusList(String orderStatus) {
+        return Arrays.stream(orderStatus.split(",")).map(String::trim).filter(StringUtils::isNumeric).map(Integer::valueOf).filter(WorkOrderStatusEnum::isValid).collect(Collectors.toList());
+    }
+
+    private String content(WorkOrderOperateDTO dto) {
+        return dto == null ? null : dto.getContent();
+    }
+
+    private String attach(WorkOrderOperateDTO dto) {
+        return dto == null ? null : dto.getAttachUrl();
+    }
+
+    private String buildDesc(String prefix, String detail) {
+        boolean prefixEmpty = StringUtils.isEmpty(prefix);
+        boolean detailEmpty = StringUtils.isEmpty(detail);
+        return prefixEmpty ? detail : (detailEmpty ? prefix : prefix + ":" + detail);
+    }
+
+    private List<String> splitAttach(String attachUrl) {
+        if (StringUtils.isEmpty(attachUrl)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(attachUrl.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
+    }
 }

+ 13 - 17
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImpl.java

@@ -18,6 +18,8 @@ import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.vo.*;
 import com.zksy.base.event.DeviceStatusChangePublisher;
 import com.zksy.base.environment.domain.ERealTimeData;
+import com.zksy.base.enums.WorkOrderStatusEnum;
+import com.zksy.ops.dto.WorkOrderDispatchDTO;
 import com.zksy.base.environment.mapper.ERealTimeDataMapper;
 import com.zksy.base.gas.domain.GasMonitorData;
 import com.zksy.base.gas.mapper.GasMonitorDataMapper;
@@ -451,8 +453,11 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         if (vo.getReceiveUserId() == null) {
             return true;
         }
-        return businessWorkOrderService.dispatchOrder(workOrder.getOrderId(), vo.getReceiveUserId(),
-                vo.getMaintainRemark(), currentUserIdOrNull(), currentUserNameOrNull());
+        WorkOrderDispatchDTO dispatchDTO = new WorkOrderDispatchDTO();
+        dispatchDTO.setOrderId(workOrder.getOrderId());
+        dispatchDTO.setHandlerId(vo.getReceiveUserId());
+        dispatchDTO.setRemarks(vo.getMaintainRemark());
+        return businessWorkOrderService.dispatch(dispatchDTO);
     }
 
     private Long currentUserIdOrNull() {
@@ -487,10 +492,9 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         WorkOrder workOrder = new WorkOrder();
         workOrder.setDeviceId(device.getEquipmentId());
         workOrder.setDeviceCode(device.getEquipmentCode());
-        workOrder.setOrderType(1);
-        workOrder.setOrderLevel(vo.getOrderLevel() == null ? 2 : vo.getOrderLevel());
+        workOrder.setOrderLevel(vo.getOrderLevel() == null ? 1 : vo.getOrderLevel());
         workOrder.setOrderDesc(StrUtil.isNotBlank(vo.getFaultDescription()) ? vo.getFaultDescription() : vo.getMaintainContent());
-        Long orderId = businessWorkOrderService.createWorkOrder(workOrder);
+        Long orderId = businessWorkOrderService.addWorkOrder(workOrder);
         workOrder.setOrderId(orderId);
         return workOrder;
     }
@@ -930,15 +934,12 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         List<WorkOrder> orders = workOrderMapper.selectList(
                 new LambdaQueryWrapper<WorkOrder>()
                         .in(WorkOrder::getDeviceId, equipmentIds)
-                        .eq(WorkOrder::getOrderType, 1)
-                        .notIn(WorkOrder::getOrderStatus, 6, 7)
+                        .notIn(WorkOrder::getOrderStatus, WorkOrderStatusEnum.finishedCodes())
                         .orderByDesc(WorkOrder::getCreateTime)
         );
         Map<String, WorkOrder> result = new HashMap<>();
         for (WorkOrder order : orders) {
-            if (!Objects.equals(order.getOrderType(), 1)
-                    || Objects.equals(order.getOrderStatus(), 6)
-                    || Objects.equals(order.getOrderStatus(), 7)) {
+            if (WorkOrderStatusEnum.isFinished(order.getOrderStatus())) {
                 continue;
             }
             result.putIfAbsent(order.getDeviceId(), order);
@@ -990,15 +991,10 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
     }
 
     private String resolveWorkOrderPerson(WorkOrder workOrder) {
-        if (workOrder == null || StrUtil.isBlank(workOrder.getReceiveUser())) {
+        if (workOrder == null || workOrder.getReceiveUser() == null) {
             return null;
         }
-        return Arrays.stream(workOrder.getReceiveUser().split(","))
-                .map(String::trim)
-                .filter(StrUtil::isNotBlank)
-                .map(this::resolveUserName)
-                .filter(StrUtil::isNotBlank)
-                .collect(Collectors.joining("、"));
+        return resolveUserName(String.valueOf(workOrder.getReceiveUser()));
     }
 
     private String resolveUserName(String userId) {

+ 77 - 40
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WorkOrderServiceImpl.java

@@ -8,6 +8,9 @@ import com.zksy.base.domain.CaseInfo;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.enums.WorkOrderDelayApplyStatusEnum;
+import com.zksy.base.enums.WorkOrderLogOperTypeEnum;
+import com.zksy.base.enums.WorkOrderStatusEnum;
 import com.zksy.base.warning.domain.EarlyWarning;
 import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.base.mapper.CaseInfoMapper;
@@ -116,20 +119,24 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     public Long addWorkOrder(WorkOrderAddInDTO inDTO) {
         log.info("新增工单-入参:{}", inDTO);
         resolveWarningDevice(inDTO);
-        // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
+        // 同一设备存在未办结工单(非 已结案/已取消)时禁止重复建单
         if (inDTO.getDeviceId() != null) {
             Long count = this.lambdaQuery()
                     .eq(WorkOrder::getDeviceId, inDTO.getDeviceId())
-                    .notIn(WorkOrder::getOrderStatus, 6, 7)
+                    .notIn(WorkOrder::getOrderStatus, WorkOrderStatusEnum.finishedCodes())
                     .count();
             if (count > 0) {
-                throw new ServiceException("该设备已有处理中的工单,需结案或驳回后才能创建新工单");
+                throw new ServiceException("该设备已有处理中的工单,需结案或取消后才能创建新工单");
             }
         }
         WorkOrder workOrder = BeanUtil.copyProperties(inDTO, WorkOrder.class);
         workOrder.setOrderNo(generateOrderNo());
-        // 初始状态:1-待派单
-        workOrder.setOrderStatus(1);
+        // 初始状态:待派单
+        workOrder.setOrderStatus(WorkOrderStatusEnum.PENDING_DISPATCH.getCode());
+        workOrder.setDelayApplyStatus(WorkOrderDelayApplyStatusEnum.NONE.getCode());
+        if (workOrder.getFaultTime() == null) {
+            workOrder.setFaultTime(new Date());
+        }
         workOrder.setCreateTime(new Date());
         workOrder.setUpdateTime(new Date());
         if(inDTO.getCaseId() != null){
@@ -154,8 +161,8 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         this.save(workOrder);
         WorkOrderLog workOrderLog = new WorkOrderLog();
         workOrderLog.setOrderId(workOrder.getOrderId());
-        //1-创建工单
-        workOrderLog.setOperType(1);
+        // 创建工单
+        workOrderLog.setOperType(WorkOrderLogOperTypeEnum.CREATE.getCode());
         workOrderLog.setOperDesc(inDTO.getOrderDesc() + ",创建工单");
         LoginUser loginUser = SecurityUtils.getLoginUser();
         if (loginUser != null) {
@@ -163,6 +170,7 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
             workOrderLog.setOperUserName(loginUser.getUser().getNickName());
         }
         workOrderLogMapper.insert(workOrderLog);
+
         // 返回工单ID,供后续派单使用
         return workOrder.getOrderId();
     }
@@ -218,16 +226,16 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
             return emptyPage(inDTO.getPageNum(), inDTO.getPageSize());
         }
         Page<WorkOrder> page = new Page<>(inDTO.getPageNum(), inDTO.getPageSize());
-        // 经办工单状态:2-待接单 3-已接单 4-处理中 5-待验收 8-延期审核
-        List<Integer> processingStatusList = Arrays.asList(2, 3, 4, 5, 8);
+        // 经办工单状态:待接单/已接单/处理中/待验收/延期中
+        List<Integer> processingStatusList = WorkOrderStatusEnum.activeCodes();
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
                 .in(WorkOrder::getOrderStatus, processingStatusList)
                 .like(StringUtils.isNotEmpty(inDTO.getOrderNo()), WorkOrder::getOrderNo, inDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(inDTO.getCaseNo()), WorkOrder::getCaseNo, inDTO.getCaseNo())
-                .eq(inDTO.getOrderType() != null, WorkOrder::getOrderType, inDTO.getOrderType())
                 .eq(inDTO.getOrderLevel() != null, WorkOrder::getOrderLevel, inDTO.getOrderLevel())
-                .eq(inDTO.getReceiveUser() != null, WorkOrder::getReceiveUser, inDTO.getReceiveUser())
+                .eq(inDTO.getReceiveUser() != null, WorkOrder::getReceiveUser,
+                        inDTO.getReceiveUser() == null ? null : Long.valueOf(inDTO.getReceiveUser().toString()))
                 .orderByDesc(WorkOrder::getUpdateTime);
         Page<WorkOrder> rs = this.page(page, queryWrapper);
         List<WorkOrderOutDTO> outDTOList = BeanUtil.copyToList(rs.getRecords(), WorkOrderOutDTO.class);
@@ -249,14 +257,13 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
             return emptyPage(inDTO.getPageNum(), inDTO.getPageSize());
         }
         Page<WorkOrder> page = new Page<>(inDTO.getPageNum(), inDTO.getPageSize());
-        // 办结工单状态:6-已结案 7-已驳回 9-已延期
-        List<Integer> finishedStatusList = Arrays.asList(6, 7, 9);
+        // 办结工单状态:已结案、已取消
+        List<Integer> finishedStatusList = WorkOrderStatusEnum.finishedCodes();
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
                 .in(WorkOrder::getOrderStatus, finishedStatusList)
                 .like(StringUtils.isNotEmpty(inDTO.getOrderNo()), WorkOrder::getOrderNo, inDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(inDTO.getCaseNo()), WorkOrder::getCaseNo, inDTO.getCaseNo())
-                .eq(inDTO.getOrderType() != null, WorkOrder::getOrderType, inDTO.getOrderType())
                 .eq(inDTO.getOrderLevel() != null, WorkOrder::getOrderLevel, inDTO.getOrderLevel())
                 .eq(inDTO.getVerifyUser() != null, WorkOrder::getVerifyUser, inDTO.getVerifyUser())
                 .ge(inDTO.getStartTime() != null, WorkOrder::getRealFinishTime, inDTO.getStartTime())
@@ -290,32 +297,37 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         }
         WorkOrder entity = new WorkOrder();
         entity.setOrderId(inDTO.getOrderId());
-        if(inDTO.getOperType() == 2){
-            //2-派单
-            entity.setOrderStatus(2);
+        WorkOrderLogOperTypeEnum operTypeEnum = WorkOrderLogOperTypeEnum.getByCode(inDTO.getOperType());
+        if (operTypeEnum == null || operTypeEnum == WorkOrderLogOperTypeEnum.CREATE) {
+            throw new ServiceException("操作类型不合法");
+        }
+        if (operTypeEnum == WorkOrderLogOperTypeEnum.DISPATCH){
+            // 派单:待派单 → 待接单
+            entity.setOrderStatus(WorkOrderStatusEnum.PENDING_ACCEPT.getCode());
             entity.setDispatchUser(inDTO.getOperUserId());
             entity.setDispatchTime(new Date());
-            entity.setReceiveUser(inDTO.getReceiveUser().toString());
-        } else if(inDTO.getOperType() == 3){
-            //3-接单
-            entity.setOrderStatus(3);
-            entity.setReceiveUser(inDTO.getOperUserId().toString());
-        } else if(inDTO.getOperType() == 4){
-            //4-开始维修
-            entity.setOrderStatus(4);
+            entity.setReceiveUser(longValue(inDTO.getReceiveUser()));
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.ACCEPT){
+            // 接单:待接单 → 已接单
+            entity.setOrderStatus(WorkOrderStatusEnum.ACCEPTED.getCode());
+            entity.setReceiveUser(inDTO.getOperUserId());
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.PROCESS){
+            // 处置:→ 处理中
+            entity.setOrderStatus(WorkOrderStatusEnum.PROCESSING.getCode());
             if (StringUtils.isNotBlank(inDTO.getMaintenanceBeforeImg())) {
                 entity.setMaintenanceBeforeImg(inDTO.getMaintenanceBeforeImg());
             }
-        } else if(inDTO.getOperType() == 5){
-            //5-提交验收
-            entity.setOrderStatus(5);
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.SUBMIT_VERIFY){
+            // 提交验收:→ 待验收
+            entity.setOrderStatus(WorkOrderStatusEnum.PENDING_VERIFY.getCode());
             entity.setVerifyUser(workOrder.getDispatchUser());
             if (StringUtils.isNotBlank(inDTO.getMaintenanceAfterImg())) {
                 entity.setMaintenanceAfterImg(inDTO.getMaintenanceAfterImg());
             }
-        } else if(inDTO.getOperType() == 6){
-            //6-验收通过
-            entity.setOrderStatus(6);
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.VERIFY_PASS){
+            // 验收通过:→ 已结案
+            entity.setOrderStatus(WorkOrderStatusEnum.CLOSED.getCode());
+            entity.setRealFinishTime(new Date());
             entity.setVerifyUser(inDTO.getOperUserId());
             entity.setVerifyTime(new Date());
             // 同步将案件状态改为已办结
@@ -344,23 +356,48 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
                             workOrder.getOrderNo(), workOrder.getAlarmId(), e);
                 }
             }
-        } else if(inDTO.getOperType() == 7){
-            //7-驳回工单
-            entity.setOrderStatus(7);
-        } else if(inDTO.getOperType() == 8){
-            //8-申请延期
-            entity.setOrderStatus(8);
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.VERIFY_REJECT){
+            // 验收驳回:→ 退回处理中
+            entity.setOrderStatus(WorkOrderStatusEnum.PROCESSING.getCode());
+            entity.setVerifyUser(inDTO.getOperUserId());
+            entity.setVerifyTime(new Date());
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.CANCEL){
+            // 取消工单:→ 已取消
+            entity.setOrderStatus(WorkOrderStatusEnum.CANCELLED.getCode());
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.DELAY_APPLY){
+            // 申请延期:延期申请状态 → 待延期审核
+            entity.setDelayApplyStatus(WorkOrderDelayApplyStatusEnum.PENDING_AUDIT.getCode());
             entity.setDelayedReason(inDTO.getDelayedReason());
             entity.setDelayedDeadline(inDTO.getDelayedDeadline());
-        } else if(inDTO.getOperType() == 9){
-            //9-延期审核通过
-            entity.setOrderStatus(9);
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.DELAY_APPROVE){
+            // 延期审核通过:→ 延期中
+            entity.setDelayApplyStatus(WorkOrderDelayApplyStatusEnum.APPROVED.getCode());
+            entity.setOrderStatus(WorkOrderStatusEnum.DELAYING.getCode());
+        } else if(operTypeEnum == WorkOrderLogOperTypeEnum.DELAY_REJECT){
+            // 延期审核驳回
+            entity.setDelayApplyStatus(WorkOrderDelayApplyStatusEnum.REJECTED.getCode());
         }
         WorkOrderLog workOrderLog = BeanUtil.copyProperties(inDTO, WorkOrderLog.class);
+        workOrderLog.setAttachUrl(inDTO.getMaintenanceBeforeImg() != null
+                ? inDTO.getMaintenanceBeforeImg() : inDTO.getMaintenanceAfterImg());
         workOrderLog.setOperUserName(user.getNickName());
         workOrderLogMapper.insert(workOrderLog);
         return this.updateById(entity);
     }
+
+    /**
+     * 责任人入参兼容 Long / String,统一转换为 Long
+     */
+    private Long longValue(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        String text = value.toString().trim();
+        return text.isEmpty() ? null : Long.valueOf(text);
+    }
 }
 
 

+ 0 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/BusinessCockpitWorkOrderReadAdapter.java

@@ -108,8 +108,6 @@ public class BusinessCockpitWorkOrderReadAdapter implements CockpitWorkOrderRead
         return new CockpitWorkOrderSummary(
                 order.getOrderNo(),
                 order.getDeviceCode(),
-                order.getOrderType(),
-                orderTypeName(order.getOrderType()),
                 order.getOrderLevel(),
                 order.getOrderDesc(),
                 state.getCode(),

+ 0 - 8
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderSummary.java

@@ -3,8 +3,6 @@ package com.zksy.cockpit.workorder;
 public class CockpitWorkOrderSummary {
     private final String workOrderCode;
     private final String deviceCode;
-    private final Integer orderType;
-    private final String orderTypeName;
     private final Integer orderLevel;
     private final String orderDesc;
     private final Integer stateCode;
@@ -18,8 +16,6 @@ public class CockpitWorkOrderSummary {
     public CockpitWorkOrderSummary(
             String workOrderCode,
             String deviceCode,
-            Integer orderType,
-            String orderTypeName,
             Integer orderLevel,
             String orderDesc,
             Integer stateCode,
@@ -31,8 +27,6 @@ public class CockpitWorkOrderSummary {
             CockpitWorkOrderEventAssociation eventAssociation) {
         this.workOrderCode = workOrderCode;
         this.deviceCode = deviceCode;
-        this.orderType = orderType;
-        this.orderTypeName = orderTypeName;
         this.orderLevel = orderLevel;
         this.orderDesc = orderDesc;
         this.stateCode = stateCode;
@@ -46,8 +40,6 @@ public class CockpitWorkOrderSummary {
 
     public String getWorkOrderCode() { return workOrderCode; }
     public String getDeviceCode() { return deviceCode; }
-    public Integer getOrderType() { return orderType; }
-    public String getOrderTypeName() { return orderTypeName; }
     public Integer getOrderLevel() { return orderLevel; }
     public String getOrderDesc() { return orderDesc; }
     public Integer getStateCode() { return stateCode; }

+ 4 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/DrainageWarning.java

@@ -33,6 +33,10 @@ public class DrainageWarning {
     @TableField(exist = false)
     private String equipmentId;
 
+    @ApiModelProperty("设备类型业务编码,对应 equipment_type.type_id")
+    @TableField("device_code")
+    private String deviceCode;
+
     @ApiModelProperty("设备名称")
     @TableField("warning_name")
     private String equipmentName;

+ 4 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageWarningService.java

@@ -37,11 +37,12 @@ public interface DrainageWarningService extends IService<DrainageWarning> {
      * 查询历史预警(status IN HANDLED/CLOSED)
      *
      * @param historyOnly   为true时强制限定 status IN ('HANDLED','CLOSED')
+     * @param equipmentType 设备模块类型(drainage=排水),按 early_warning.device_code 匹配 equipment_type.type_id
      */
     Page<DrainageWarning> findHistoryPage(long pageNum, long pageSize,
                                           Integer warningLevel, String warningType,
                                           String status, String startTime, String endTime,
-                                          boolean historyOnly, String equipmentName);
+                                          boolean historyOnly, String equipmentName, String equipmentType);
 
     /**
      * 发布新预警(自动生成预警编号、设置发布时间和默认状态)
@@ -101,7 +102,8 @@ public interface DrainageWarningService extends IService<DrainageWarning> {
                                                   Long userId, Long deptId, boolean isAdmin,
                                                   Integer warningLevel, String warningType,
                                                   String status, boolean currentOnly,
-                                                  String equipmentName, String startTime, String endTime);
+                                                  String equipmentName, String startTime, String endTime,
+                                                  String equipmentType);
 
     /**
      * 处理(指定人员处理)

+ 78 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageWarningServiceImpl.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.base.domain.EquipmentBase;
+import com.zksy.base.mapper.EquipmentBaseMapper;
 import com.zksy.drainage.domain.DrainageWarning;
 import com.zksy.drainage.domain.WarningScopeUser;
 import com.zksy.drainage.mapper.DrainageWarningMapper;
@@ -17,7 +19,9 @@ import org.springframework.transaction.annotation.Transactional;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -34,6 +38,9 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
     @Autowired
     private WarningScopeUserMapper warningScopeUserMapper;
 
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
     private static final List<String> CURRENT_STATUSES = Arrays.asList("DRAFT", "PENDING", "PROCESSING", "RELEASED");
     private static final List<String> HISTORY_STATUSES = Arrays.asList("HANDLED", "CLOSED");
 
@@ -44,7 +51,9 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
         Page<DrainageWarning> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<DrainageWarning> wrapper = buildQueryWrapper(warningLevel, warningType, status, startTime, endTime);
         wrapper.orderByDesc(DrainageWarning::getCreateTime);
-        return this.page(page, wrapper);
+        Page<DrainageWarning> result = this.page(page, wrapper);
+        fillMissingCoordinates(result);
+        return result;
     }
 
     @Override
@@ -62,14 +71,16 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
             wrapper.eq(DrainageWarning::getWarningType, warningType);
         }
         wrapper.orderByDesc(DrainageWarning::getCreateTime);
-        return this.page(page, wrapper);
+        Page<DrainageWarning> result = this.page(page, wrapper);
+        fillMissingCoordinates(result);
+        return result;
     }
 
     @Override
     public Page<DrainageWarning> findHistoryPage(long pageNum, long pageSize,
                                                   Integer warningLevel, String warningType,
                                                   String status, String startTime, String endTime,
-                                                  boolean historyOnly, String equipmentName) {
+                                                  boolean historyOnly, String equipmentName, String equipmentType) {
         Page<DrainageWarning> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<DrainageWarning> wrapper = new LambdaQueryWrapper<>();
         // 历史查询默认只返回已处置或已解除的记录;传入 status 时允许
@@ -77,6 +88,14 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
         if (historyOnly && (status == null || status.isEmpty())) {
             wrapper.in(DrainageWarning::getStatus, HISTORY_STATUSES);
         }
+        // device_code 保存设备类型业务编码;历史预警按排水设备类型树筛选。
+        if ("drainage".equalsIgnoreCase(equipmentType)) {
+            wrapper.inSql(DrainageWarning::getDeviceCode,
+                    "SELECT et.type_id FROM app_user.equipment_type et "
+                            + "WHERE et.type_id IS NOT NULL AND "
+                            + "(et.type_id = 'drainage' OR et.parent_type_id = '2' OR et.parent_type_id IN "
+                            + "(SELECT id FROM app_user.equipment_type WHERE type_id = 'drainage'))");
+        }
         // 附加筛选条件
         if (warningLevel != null) {
             wrapper.eq(DrainageWarning::getWarningLevel, warningLevel);
@@ -97,7 +116,9 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
             wrapper.le(DrainageWarning::getCreateTime, LocalDateTime.parse(endTime, DATE_TIME_FORMATTER));
         }
         wrapper.orderByDesc(DrainageWarning::getCreateTime);
-        return this.page(page, wrapper);
+        Page<DrainageWarning> result = this.page(page, wrapper);
+        fillMissingCoordinates(result);
+        return result;
     }
 
     @Override
@@ -180,7 +201,8 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
                                                          Long userId, Long deptId, boolean isAdmin,
                                                          Integer warningLevel, String warningType,
                                                          String status, boolean currentOnly,
-                                                         String equipmentName, String startTime, String endTime) {
+                                                         String equipmentName, String startTime, String endTime,
+                                                         String equipmentType) {
         Page<DrainageWarning> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<DrainageWarning> wrapper = new LambdaQueryWrapper<>();
 
@@ -201,6 +223,14 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
         if (warningType != null && !warningType.isEmpty()) {
             wrapper.eq(DrainageWarning::getWarningType, warningType);
         }
+        // device_code 保存设备类型的 type_id;当前预警按排水设备类型树筛选。
+        if ("drainage".equalsIgnoreCase(equipmentType)) {
+            wrapper.inSql(DrainageWarning::getDeviceCode,
+                    "SELECT et.type_id FROM app_user.equipment_type et "
+                            + "WHERE et.type_id IS NOT NULL AND "
+                            + "(et.type_id = 'drainage' OR et.parent_type_id = '2' OR et.parent_type_id IN "
+                            + "(SELECT id FROM app_user.equipment_type WHERE type_id = 'drainage'))");
+        }
         if (equipmentName != null && !equipmentName.isEmpty()) {
             wrapper.like(DrainageWarning::getEquipmentName, equipmentName);
         }
@@ -212,7 +242,9 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
         }
 
         wrapper.orderByDesc(DrainageWarning::getCreateTime);
-        return this.page(page, wrapper);
+        Page<DrainageWarning> result = this.page(page, wrapper);
+        fillMissingCoordinates(result);
+        return result;
     }
 
     @Override
@@ -294,4 +326,44 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
         }
         return wrapper;
     }
+
+    /**
+     * 预警历史数据可能只保存了设备名称,坐标为空时从设备基础表补齐真实坐标,
+     * 供列表和 GIS 页面统一使用。仅补齐缺失坐标,不覆盖预警表已有坐标。
+     */
+    private void fillMissingCoordinates(Page<DrainageWarning> page) {
+        if (page == null || page.getRecords() == null || page.getRecords().isEmpty()) {
+            return;
+        }
+        List<String> names = page.getRecords().stream()
+                .filter(item -> item.getLongitude() == null || item.getLatitude() == null)
+                .map(DrainageWarning::getEquipmentName)
+                .filter(name -> name != null && !name.trim().isEmpty())
+                .distinct()
+                .collect(Collectors.toList());
+        if (names.isEmpty()) {
+            return;
+        }
+        List<EquipmentBase> equipments = equipmentBaseMapper.selectList(
+                new LambdaQueryWrapper<EquipmentBase>().in(EquipmentBase::getEquipmentName, names));
+        Map<String, EquipmentBase> equipmentMap = new HashMap<>();
+        for (EquipmentBase equipment : equipments) {
+            if (equipment.getLongitude() != null && equipment.getLatitude() != null) {
+                equipmentMap.putIfAbsent(equipment.getEquipmentName(), equipment);
+            }
+        }
+        for (DrainageWarning warning : page.getRecords()) {
+            if (warning.getLongitude() != null && warning.getLatitude() != null) {
+                continue;
+            }
+            EquipmentBase equipment = equipmentMap.get(warning.getEquipmentName());
+            if (equipment != null) {
+                warning.setLongitude(equipment.getLongitude());
+                warning.setLatitude(equipment.getLatitude());
+                if (warning.getLocation() == null || warning.getLocation().trim().isEmpty()) {
+                    warning.setLocation(equipment.getEquipmentLocation());
+                }
+            }
+        }
+    }
 }

+ 38 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDelayApplyDTO.java

@@ -0,0 +1,38 @@
+package com.zksy.ops.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 工单延期申请入参
+ *
+ * @author zksy
+ */
+@Data
+@ApiModel(value = "WorkOrderDelayApplyDTO", description = "工单延期申请入参")
+public class WorkOrderDelayApplyDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 工单ID */
+    @NotNull(message = "工单ID不能为空")
+    @ApiModelProperty(value = "工单ID", required = true)
+    private Long orderId;
+
+    /** 延期截止时间 */
+    @NotNull(message = "延期截止时间不能为空")
+    @ApiModelProperty(value = "延期截止时间", required = true)
+    private String delayedDeadline;
+
+    /** 延期原因 */
+    @ApiModelProperty(value = "延期原因", required = true)
+    private String delayedReason;
+
+    /** 延期申请附件 URL,多个使用逗号分隔 */
+    @ApiModelProperty(value = "延期申请附件URL,多个使用逗号分隔")
+    private String attachUrl;
+}

+ 34 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDelayAuditDTO.java

@@ -0,0 +1,34 @@
+package com.zksy.ops.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 工单延期审核入参
+ *
+ * @author zksy
+ */
+@Data
+@ApiModel(value = "WorkOrderDelayAuditDTO", description = "工单延期审核入参")
+public class WorkOrderDelayAuditDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 工单ID */
+    @NotNull(message = "工单ID不能为空")
+    @ApiModelProperty(value = "工单ID", required = true)
+    private Long orderId;
+
+    /** 审核结果:true-通过 false-驳回 */
+    @NotNull(message = "审核结果不能为空")
+    @ApiModelProperty(value = "审核结果:true-通过 false-驳回", required = true)
+    private Boolean approved;
+
+    /** 审核意见 */
+    @ApiModelProperty(value = "审核意见")
+    private String auditOpinion;
+}

+ 29 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDetailVO.java

@@ -0,0 +1,29 @@
+package com.zksy.ops.dto;
+
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 工单详情出参(工单信息 + 操作日志)
+ *
+ * @author zksy
+ */
+@Data
+@ApiModel(value = "WorkOrderDetailVO", description = "工单详情出参")
+public class WorkOrderDetailVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "工单信息")
+    private WorkOrder order;
+
+    @ApiModelProperty(value = "操作日志")
+    private List<WorkOrderLog> logs = new ArrayList<>();
+}

+ 34 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDispatchDTO.java

@@ -0,0 +1,34 @@
+package com.zksy.ops.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 工单派单入参
+ *
+ * @author zksy
+ */
+@Data
+@ApiModel(value = "WorkOrderDispatchDTO", description = "工单派单入参")
+public class WorkOrderDispatchDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 工单ID */
+    @NotNull(message = "工单ID不能为空")
+    @ApiModelProperty(value = "工单ID", required = true)
+    private Long orderId;
+
+    /** 责任人用户ID */
+    @NotNull(message = "责任人不能为空")
+    @ApiModelProperty(value = "责任人用户ID", required = true)
+    private Long handlerId;
+
+    /** 派单备注 */
+    @ApiModelProperty(value = "派单备注")
+    private String remarks;
+}

+ 33 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderOperateDTO.java

@@ -0,0 +1,33 @@
+package com.zksy.ops.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 工单通用操作入参(核查接单 / 处置 / 提交验收 / 取消工单)
+ *
+ * @author zksy
+ */
+@Data
+@ApiModel(value = "WorkOrderOperateDTO", description = "工单通用操作入参")
+public class WorkOrderOperateDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 工单ID */
+    @NotNull(message = "工单ID不能为空")
+    @ApiModelProperty(value = "工单ID", required = true)
+    private Long orderId;
+
+    /** 操作内容:核查结论 / 处置内容 / 验收说明 / 取消原因 */
+    @ApiModelProperty(value = "操作内容:核查结论/处置内容/验收说明/取消原因")
+    private String content;
+
+    /** 操作附件 URL,多个使用逗号分隔 */
+    @ApiModelProperty(value = "操作附件URL,多个使用逗号分隔")
+    private String attachUrl;
+}

+ 71 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderPageDTO.java

@@ -0,0 +1,71 @@
+package com.zksy.ops.dto;
+
+import com.zksy.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 工单分页查询入参
+ *
+ * <p>继承 {@link BaseEntity} 以接入框架 {@code DataScopeAspect} 数据权限:切面会把生成的数据范围 SQL
+ * 写入 {@code params["dataScope"]},由 Service 通过 {@code QueryWrapper#apply} 注入查询条件。
+ * 若不继承 BaseEntity,切面无法写入该参数,数据权限会静默失效。
+ *
+ * @author zksy
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel(value = "WorkOrderPageDTO", description = "工单分页查询入参")
+public class WorkOrderPageDTO extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 页码,默认 1 */
+    @ApiModelProperty(value = "页码")
+    private Long pageNum = 1L;
+
+    /** 每页条数,默认 10 */
+    @ApiModelProperty(value = "每页条数")
+    private Long pageSize = 10L;
+
+    /** 工单优先级:1-紧急 2-一般 3-低 */
+    @ApiModelProperty(value = "工单优先级:1-紧急 2-一般 3-低")
+    private Integer orderLevel;
+
+    /** 工单状态:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消,支持逗号分隔多值 */
+    @ApiModelProperty(value = "工单状态,支持逗号分隔多值")
+    private String orderStatus;
+
+    /** 延期申请状态:0-无 1-待审核 2-通过 3-驳回 */
+    @ApiModelProperty(value = "延期申请状态:0-无延期申请 1-待延期审核 2-延期审核通过 3-延期审核驳回")
+    private Integer delayApplyStatus;
+
+    /** 设备编码 */
+    @ApiModelProperty(value = "设备编码")
+    private String deviceCode;
+
+    /** 关键字:工单编号 / 设备编码 / 故障描述 */
+    @ApiModelProperty(value = "关键字:工单编号/设备编码/故障描述")
+    private String keyword;
+
+    /** 故障发生时间-开始 */
+    @ApiModelProperty(value = "故障发生时间起,yyyy-MM-dd HH:mm:ss")
+    private String faultTimeStart;
+
+    /** 故障发生时间-结束 */
+    @ApiModelProperty(value = "故障发生时间止,yyyy-MM-dd HH:mm:ss")
+    private String faultTimeEnd;
+
+    /**
+     * 设备父类别ID,用于按业务口径收窄工单范围(如燃气工单管理页只查燃气类设备的工单)。
+     *
+     * <p>过滤链路:work_order.device_id → equipment_base.equipment_id
+     * → equipment_base.equipment_type_id → equipment_type.id → equipment_type.parent_type_id
+     *
+     * <p>为空表示不限制,兼容原有的全量工单查询与各既有调用方。
+     */
+    @ApiModelProperty(value = "设备父类别ID,用于仅查询该类别下设备产生的工单,为空表示不限制")
+    private String deviceTypeParentId;
+}

+ 38 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderVerifyDTO.java

@@ -0,0 +1,38 @@
+package com.zksy.ops.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 工单验收入参
+ *
+ * @author zksy
+ */
+@Data
+@ApiModel(value = "WorkOrderVerifyDTO", description = "工单验收入参")
+public class WorkOrderVerifyDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 工单ID */
+    @NotNull(message = "工单ID不能为空")
+    @ApiModelProperty(value = "工单ID", required = true)
+    private Long orderId;
+
+    /** 是否验收通过:true-通过(已结案) false-驳回(回到处理中) */
+    @NotNull(message = "验收结果不能为空")
+    @ApiModelProperty(value = "是否验收通过:true-通过 false-驳回", required = true)
+    private Boolean passed;
+
+    /** 验收意见 */
+    @ApiModelProperty(value = "验收意见")
+    private String content;
+
+    /** 验收附件 URL,多个使用逗号分隔 */
+    @ApiModelProperty(value = "验收附件URL,多个使用逗号分隔")
+    private String attachUrl;
+}

+ 195 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/support/OpsAssignScopeSupport.java

@@ -0,0 +1,195 @@
+package com.zksy.ops.support;
+
+import com.zksy.common.core.domain.TreeSelect;
+import com.zksy.common.core.domain.entity.SysDept;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.common.utils.StringUtils;
+import com.zksy.system.service.ISysDeptService;
+import com.zksy.system.service.ISysUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 运维模块「可指派范围」公共支撑组件。
+ *
+ * <p>统一维护:当前登录用户ID/部门ID、可见部门范围、可指派部门树与可指派用户列表,
+ * 供巡检任务、工单等同属 ops 的业务复用,避免各 Service 重复实现。</p>
+ *
+ * @author zksy
+ */
+@Slf4j
+@Component
+public class OpsAssignScopeSupport {
+
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    /**
+     * 获取当前登录用户ID,未登录时返回 null(避免鉴权异常掩盖业务错误)
+     */
+    public Long currentUserId() {
+        try {
+            return SecurityUtils.getUserId();
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取当前登录用户所属部门ID,未登录时返回 null
+     */
+    public Long currentDeptId() {
+        try {
+            return SecurityUtils.getDeptId();
+        } catch (Exception e) {
+            log.warn("获取当前登录用户部门ID失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 当前登录用户是否为超级管理员
+     */
+    public boolean isAdmin() {
+        Long userId = currentUserId();
+        return userId != null && SecurityUtils.isAdmin(userId);
+    }
+
+    /**
+     * 获取当前登录用户可指派的部门ID集合:本部门 + 全部子孙部门 + 同级部门(同一父部门下的其他部门)。
+     * 顶级部门(parentId=0)不做同级扩展,避免范围放大到全部一级部门。
+     *
+     * @return null 表示超级管理员,不做数据限制;空集合表示无部门范围
+     */
+    public Set<Long> scopeDeptIds() {
+        if (isAdmin()) {
+            return null;
+        }
+        Long currentDeptId = currentDeptId();
+        Set<Long> allowed = new HashSet<>();
+        if (currentDeptId == null) {
+            return allowed;
+        }
+        List<SysDept> allDepts = deptService.selectDeptList(new SysDept());
+        if (allDepts == null || allDepts.isEmpty()) {
+            return allowed;
+        }
+        // 取当前部门的父ID,用于纳入同级部门(顶级部门 parentId=0 不做同级扩展,避免范围放大到全部一级部门)
+        Long parentId = null;
+        for (SysDept dept : allDepts) {
+            if (currentDeptId.equals(dept.getDeptId())) {
+                parentId = dept.getParentId();
+                break;
+            }
+        }
+        boolean hasSibling = parentId != null && parentId != 0L;
+        for (SysDept dept : allDepts) {
+            if (dept.getDeptId() == null) {
+                continue;
+            }
+            // 本部门、子孙部门
+            if (dept.getDeptId().equals(currentDeptId) || ancestorsContain(dept.getAncestors(), currentDeptId)) {
+                allowed.add(dept.getDeptId());
+                continue;
+            }
+            // 同级部门(同一父部门下的其他部门)
+            if (hasSibling && parentId.equals(dept.getParentId())) {
+                allowed.add(dept.getDeptId());
+            }
+        }
+        return allowed;
+    }
+
+    /**
+     * 获取可指派部门树:当前登录用户所属部门及其全部子部门
+     */
+    public List<TreeSelect> assignableDeptTree() {
+        Set<Long> scope = scopeDeptIds();
+        List<SysDept> allDepts = deptService.selectDeptList(new SysDept());
+        if (allDepts == null || allDepts.isEmpty()) {
+            return Collections.emptyList();
+        }
+        if (scope == null) {
+            return deptService.buildDeptTreeSelect(allDepts);
+        }
+        if (scope.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<SysDept> scoped = allDepts.stream()
+                .filter(dept -> dept.getDeptId() != null && scope.contains(dept.getDeptId()))
+                .collect(Collectors.toList());
+        return deptService.buildDeptTreeSelect(scoped);
+    }
+
+    /**
+     * 查询可指派用户:仅返回指定部门(须在可指派部门范围内)的人员
+     */
+    public List<SysUser> assignableUsers(Long deptId) {
+        Set<Long> scope = scopeDeptIds();
+        if (scope == null) {
+            SysUser query = new SysUser();
+            if (deptId != null) {
+                query.setDeptId(deptId);
+            }
+            return userService.selectUserList(query);
+        }
+        if (scope.isEmpty()) {
+            return new ArrayList<>();
+        }
+        Long targetDeptId = (deptId == null || !scope.contains(deptId)) ? currentDeptId() : deptId;
+        if (targetDeptId == null || !scope.contains(targetDeptId)) {
+            throw new ServiceException("无权限获取该部门人员");
+        }
+        SysUser query = new SysUser();
+        query.setDeptId(targetDeptId);
+        return userService.selectUserList(query);
+    }
+
+    /**
+     * 校验被指派人是否处于当前登录用户可指派的部门范围内,否则抛出异常
+     */
+    public void validateAssignUserInScope(Long assignUserId) {
+        if (assignUserId == null) {
+            return;
+        }
+        Set<Long> scope = scopeDeptIds();
+        if (scope == null) {
+            return;
+        }
+        if (scope.isEmpty()) {
+            throw new ServiceException("无法校验指派人员部门范围,请联系管理员");
+        }
+        SysUser assignUser = userService.selectUserById(assignUserId);
+        if (assignUser == null || assignUser.getDeptId() == null) {
+            throw new ServiceException("指派人不存在或部门信息缺失");
+        }
+        if (!scope.contains(assignUser.getDeptId())) {
+            throw new ServiceException("只能指派当前部门、同级部门或下属部门的成员");
+        }
+    }
+
+    /**
+     * 判断部门祖先链是否包含指定部门ID
+     */
+    private boolean ancestorsContain(String ancestors, Long deptId) {
+        if (StringUtils.isEmpty(ancestors) || deptId == null) {
+            return false;
+        }
+        for (String part : ancestors.split(",")) {
+            if (StringUtils.isNotEmpty(part.trim()) && deptId.toString().equals(part.trim())) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

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

@@ -26,6 +26,14 @@ public interface SysUserMapper
      */
     public List<SysUser> selectAllEnabledUser();
 
+    /**
+     * 按用户ID批量查询有效用户(未删除且状态正常),用于按需回填姓名,避免全量拉取用户表
+     *
+     * @param userIds 用户ID集合
+     * @return 用户信息集合
+     */
+    public List<SysUser> selectEnabledUsersByIds(@Param("userIds") java.util.Collection<Long> userIds);
+
     /**
      * 根据条件分页查询已配用户角色列表
      * 

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

@@ -25,6 +25,14 @@ public interface ISysUserService
      */
     public List<SysUser> selectAllEnabledUser();
 
+    /**
+     * 按用户ID批量查询有效用户(未删除且状态正常)
+     *
+     * @param userIds 用户ID集合,为空时返回空集合
+     * @return 用户信息集合
+     */
+    public List<SysUser> selectEnabledUsersByIds(java.util.Collection<Long> userIds);
+
     /**
      * 根据条件分页查询已分配用户角色列表
      * 

+ 16 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/impl/SysUserServiceImpl.java

@@ -89,6 +89,22 @@ public class SysUserServiceImpl implements ISysUserService
         return userMapper.selectAllEnabledUser();
     }
 
+    /**
+     * 按用户ID批量查询有效用户(未删除且状态正常,不做数据权限过滤)
+     *
+     * @param userIds 用户ID集合,为空时返回空集合
+     * @return 用户信息集合
+     */
+    @Override
+    public List<SysUser> selectEnabledUsersByIds(java.util.Collection<Long> userIds)
+    {
+        if (userIds == null || userIds.isEmpty())
+        {
+            return new java.util.ArrayList<>();
+        }
+        return userMapper.selectEnabledUsersByIds(userIds);
+    }
+
     /**
      * 根据条件分页查询已分配用户角色列表
      * 

+ 9 - 7
pipe-network-service/zksy-system/src/main/resources/mapper/WorkOrderMapper.xml

@@ -12,14 +12,15 @@
             <result property="caseNo" column="case_no" jdbcType="VARCHAR"/>
             <result property="deviceId" column="device_id" jdbcType="VARCHAR"/>
             <result property="deviceCode" column="device_code" jdbcType="VARCHAR"/>
-            <result property="orderType" column="order_type" jdbcType="TINYINT"/>
             <result property="orderLevel" column="order_level" jdbcType="TINYINT"/>
             <result property="orderDesc" column="order_desc" jdbcType="VARCHAR"/>
             <result property="deptId" column="dept_id" jdbcType="BIGINT"/>
             <result property="dispatchUser" column="dispatch_user" jdbcType="BIGINT"/>
             <result property="dispatchTime" column="dispatch_time" jdbcType="TIMESTAMP"/>
-            <result property="receiveUser" column="receive_user" jdbcType="VARCHAR"/>
+            <result property="receiveUser" column="receive_user" jdbcType="BIGINT"/>
             <result property="orderStatus" column="order_status" jdbcType="TINYINT"/>
+            <result property="delayApplyStatus" column="delay_apply_status" jdbcType="TINYINT"/>
+            <result property="faultTime" column="fault_time" jdbcType="TIMESTAMP"/>
             <result property="delayedReason" column="delayed_reason" jdbcType="VARCHAR"/>
             <result property="delayedDeadline" column="delayed_deadline" jdbcType="TIMESTAMP"/>
             <result property="planFinishTime" column="plan_finish_time" jdbcType="TIMESTAMP"/>
@@ -34,23 +35,24 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        order_id,order_no,alarm_id,
+        order_id,order_no,alarm_id,hazard_id,hazard_no,
         case_id,case_no,device_id,
-        device_code,order_type,order_level,
+        device_code,order_level,fault_time,
         order_desc,dept_id,dispatch_user,
         dispatch_time,receive_user,order_status,
-        delayed_reason,delayed_deadline,plan_finish_time,
+        delay_apply_status,delayed_reason,delayed_deadline,plan_finish_time,
         real_finish_time,repair_result,maintenance_before_img,maintenance_after_img,verify_user,
         verify_time,create_time,update_time
     </sql>
 
+    <!-- 待派单(0)/待接单(1) 始终提醒;其余未办结状态仅在超时后提醒 -->
     <select id="selectPendingOrOverdue" resultMap="BaseResultMap">
         SELECT
         <include refid="Base_Column_List"/>
         FROM app_user.work_order
-        WHERE order_status IN (1, 2)
+        WHERE order_status IN (0, 1)
            OR (
-                order_status IN (3, 4, 5, 8, 9)
+                order_status IN (2, 3, 4, 6)
                 AND COALESCE(delayed_deadline, plan_finish_time) &lt; #{now}
            )
         ORDER BY order_level ASC NULLS LAST,

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

@@ -96,6 +96,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by u.nick_name
     </select>
 
+    <select id="selectEnabledUsersByIds" resultMap="SysUserResult">
+        select u.user_id, u.dept_id, u.nick_name, u.user_name
+        from app_user.sys_user u
+        where u.del_flag = '0' and u.status = '0'
+        and u.user_id in
+        <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </select>
+
     <select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">
 	    select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
 	    from app_user.sys_user u