zengy 1 сар өмнө
parent
commit
7a2dcd80d6
18 өөрчлөгдсөн 1338 нэмэгдсэн , 0 устгасан
  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. 183 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/ops/PworkOrderController.java
  7. 52 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderDelayApplyStatusEnum.java
  8. 60 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderLevelEnum.java
  9. 68 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderLogOperTypeEnum.java
  10. 146 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/enums/WorkOrderStatusEnum.java
  11. 38 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDelayApplyDTO.java
  12. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDelayAuditDTO.java
  13. 29 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDetailVO.java
  14. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDispatchDTO.java
  15. 33 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderOperateDTO.java
  16. 71 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderPageDTO.java
  17. 38 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderVerifyDTO.java
  18. 195 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/support/OpsAssignScopeSupport.java

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

+ 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("删除失败");
+    }
+}

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

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