Przeglądaj źródła

优化工单,报警查询

zengy 1 tydzień temu
rodzic
commit
34d507a779
24 zmienionych plików z 377 dodań i 209 usunięć
  1. 2 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/common/MinioController.java
  2. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/WaterAlarmOutDTO.java
  3. 6 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java
  4. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/AlarmData.java
  5. 1 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmReviewQuery.java
  6. 33 7
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmOperationsService.java
  7. 33 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewService.java
  8. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/MaintainOrderVO.java
  9. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/domain/WorkOrder.java
  10. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/domain/vo/WorkOrderPushVO.java
  11. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/domain/vo/WorkOrderVO.java
  12. 2 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderAddDTO.java
  13. 5 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderDispatchDTO.java
  14. 2 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderPageDTO.java
  15. 0 60
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/enums/WorkOrderLevelEnum.java
  16. 110 31
      pipe-network-service/zksy-system/src/main/java/com/zksy/ops/service/impl/WorkOrderServiceImpl.java
  17. 6 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/MonitorAlarm/WaterSupplyAlarmRealtimeContractTest.java
  18. 22 1
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmOperationsServiceTest.java
  19. 10 1
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmReviewServiceTest.java
  20. 132 1
      pipe-network-service/zksy-system/src/test/java/com/zksy/ops/service/impl/WorkOrderServiceImplTest.java
  21. 1 1
      zk-api-service/src/main/java/com/zksy/api/domain/AlarmData.java
  22. 1 1
      zk-api-service/src/main/java/com/zksy/api/domain/WorkOrder.java
  23. 5 23
      zk-api-service/src/main/java/com/zksy/api/domain/WorkOrderLog.java
  24. 0 60
      zk-api-service/src/main/java/com/zksy/api/enums/WorkOrderLevelEnum.java

+ 2 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/common/MinioController.java

@@ -71,6 +71,7 @@ public class MinioController
 
             AjaxResult ajax = AjaxResult.success();
             ajax.put("url", fullUrl);
+            ajax.put("relativePath", "/" + relativePath.replaceFirst("^/+", ""));
             ajax.put("fileName", fileName);
             ajax.put("originalFilename", file.getOriginalFilename());
             ajax.put("contentType", file.getContentType());
@@ -121,6 +122,7 @@ public class MinioController
 
                 Map<String, Object> fileInfo = new HashMap<>();
                 fileInfo.put("url", fullUrl);
+                fileInfo.put("relativePath", "/" + relativePath.replaceFirst("^/+", ""));
                 fileInfo.put("fileName", fileName);
                 fileInfo.put("originalFilename", file.getOriginalFilename());
                 fileInfo.put("contentType", file.getContentType());

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

@@ -60,7 +60,7 @@ public class WaterAlarmOutDTO implements Serializable {
     @ApiModelProperty(value = "偏差率(百分比)", example = "20.00")
     private BigDecimal deviationRatio;
 
-    @ApiModelProperty(value = "报警等级:1-4,1级最高", example = "1")
+    @ApiModelProperty(value = "报警等级:alarm_level 字典,保留1-4计算编码,1级最高", example = "1")
     private Integer alarmLevel;
 
     @ApiModelProperty(value = "报警等级文本", example = "一级报警")

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

@@ -11,7 +11,7 @@ import com.zksy.base.service.impl.WaterSupplyQueryService;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.enums.AlarmAuditStatusEnum;
 import com.zksy.base.alarm.enums.AlarmDisplayStatusEnum;
-import com.zksy.base.alarm.enums.AlarmLevelEnum;
+import com.zksy.system.service.ISysDictDataService;
 import com.zksy.base.alarm.enums.AlarmRecordStatusEnum;
 import com.zksy.ops.enums.WorkOrderStatusEnum;
 import com.zksy.base.alarm.service.AlarmDataService;
@@ -46,6 +46,9 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     @Autowired
     private AlarmDataService alarmDataService;
 
+    @Autowired
+    private ISysDictDataService dictDataService;
+
     @Autowired
     private com.zksy.base.alarm.mapper.AlarmDataMapper alarmDataMapper;
 
@@ -257,7 +260,7 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     }
 
     private String formatAlarmLevel(Integer alarmLevel) {
-        AlarmLevelEnum level = AlarmLevelEnum.fromCode(alarmLevel);
-        return level == null ? "未知等级" : level.getLabel();
+        String label = alarmLevel == null ? null : dictDataService.selectDictLabel("alarm_level", String.valueOf(alarmLevel));
+        return StrUtil.isBlank(label) ? "未知等级" : label;
     }
 }

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

@@ -60,7 +60,7 @@ public class AlarmData implements Serializable {
     private BigDecimal actualValue;
 
     @TableField(value = "alarm_level")
-    @ApiModelProperty(value = "告警等级 1-4,1级最高")
+    @ApiModelProperty(value = "报警等级:alarm_level 字典,保留1-4计算编码,1级最高,名称与样式由字典配置")
     private Integer alarmLevel;
 
     @TableField(value = "deviation_ratio")

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

@@ -17,6 +17,7 @@ public class AlarmReviewQuery {
     @ApiModelProperty("设备编码/名称,模糊匹配")
     @Size(max = 100)
     private String keyword;
+    @ApiModelProperty("报警等级:alarm_level 字典启用值,保留1-4编码,1级最高")
     private Integer alarmLevel;
     private Integer auditStatus;
     @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss[.SSSSSS]")

+ 33 - 7
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmOperationsService.java

@@ -9,6 +9,8 @@ import com.zksy.base.alarm.mapper.*;
 import com.zksy.base.enums.EquipmentTypeEnum;
 import com.zksy.base.enums.AlarmStatusEnum;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.common.core.domain.entity.SysDictData;
+import com.zksy.system.service.ISysDictDataService;
 import com.zksy.ops.enums.WorkOrderStatusEnum;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -24,11 +26,12 @@ public class AlarmOperationsService {
     private final AlarmOperationsMapper mapper;
     private final AlarmReviewScopeService scope;
     private final AlarmLogMapper logs;
+    private final ISysDictDataService dictDataService;
 
     public Page<AlarmVO> page(AlarmQuery query) {
         validate(query);
         Page<AlarmVO> page = mapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), query, scope.resolveOperations(query));
-        page.getRecords().forEach(this::labels);
+        labels(page.getRecords());
         return page;
     }
     public AlarmVO detail(String id, AlarmQuery query) {
@@ -42,7 +45,7 @@ public class AlarmOperationsService {
         query.setAlarmStatus(AlarmRecordStatusEnum.UNHANDLED.getCode());
         validate(query);
         Page<AlarmVO> page = mapper.selectReminders(new Page<>(query.getPageNum(), query.getPageSize()), query, scope.resolveOperations(query));
-        page.getRecords().forEach(this::labels);
+        labels(page.getRecords());
         return page;
     }
     public List<AlarmLog> logs(String id, AlarmQuery query) {
@@ -94,7 +97,7 @@ public class AlarmOperationsService {
         validate(query);
         List<AlarmVO> result = mapper.selectMap(query, scope.resolveOperations(query));
         if (result.size() > 10000) throw new ServiceException("报警数量超过地图展示上限,请缩小查询范围");
-        result.forEach(this::labels);
+        labels(result);
         return result;
     }
     public Map<String, Object> analysis(String id, AlarmQuery query, AlarmSeriesPeriod period) {
@@ -144,7 +147,7 @@ public class AlarmOperationsService {
     public Map<String, Object> options(AlarmQuery query) {
         scope.resolveOperations(query);
         Map<String, Object> result = new LinkedHashMap<>();
-        result.put("alarmLevels", Arrays.stream(AlarmLevelEnum.values()).map(v -> option(v.getCode(), v.getLabel())).collect(Collectors.toList()));
+        result.put("alarmLevels", alarmLevels().stream().map(v -> option(Integer.parseInt(v.getDictValue()), v.getDictLabel())).collect(Collectors.toList()));
         result.put("auditStatuses", Arrays.stream(AlarmAuditStatusEnum.values()).map(v -> option(v.getCode(), v.getLabel())).collect(Collectors.toList()));
         result.put("monitorStatuses", Arrays.stream(AlarmRecordStatusEnum.values()).map(v -> option(v.getCode(),
                 v == AlarmRecordStatusEnum.UNHANDLED ? "异常" : "已恢复")).collect(Collectors.toList()));
@@ -160,17 +163,40 @@ public class AlarmOperationsService {
     }
     private void validate(AlarmQuery query) {
         if (query.getPageNum() < 1 || query.getPageSize() < 1 || query.getPageSize() > 100) throw new ServiceException("分页参数无效");
-        if (query.getAlarmLevel() != null && AlarmLevelEnum.fromCode(query.getAlarmLevel()) == null) throw new ServiceException("报警等级无效");
+        if (query.getAlarmLevel() != null && alarmLevels().stream().noneMatch(v -> String.valueOf(query.getAlarmLevel()).equals(v.getDictValue()))) throw new ServiceException("报警等级无效");
         if (query.getAuditStatus() != null && AlarmAuditStatusEnum.fromCode(query.getAuditStatus()) == null) throw new ServiceException("审核结果无效");
         if (query.getAlarmStatus() != null && AlarmRecordStatusEnum.fromCode(query.getAlarmStatus()) == null) throw new ServiceException("监测状态无效");
         if (query.getDisplayStatus() != null && (query.getDisplayStatus() < 1 || query.getDisplayStatus() > 4)) throw new ServiceException("报警展示状态无效");
         if (query.getDeviceAlarmStatus() != null && query.getDeviceAlarmStatus() != 0 && query.getDeviceAlarmStatus() != 1) throw new ServiceException("设备报警状态无效");
         if (query.getStartTime() != null && query.getEndTime() != null && query.getStartTime().isAfter(query.getEndTime())) throw new ServiceException("开始时间不能晚于结束时间");
     }
+    private List<SysDictData> alarmLevels() {
+        SysDictData query = new SysDictData();
+        query.setDictType("alarm_level");
+        query.setStatus("0");
+        List<SysDictData> values = dictDataService.selectDictDataList(query);
+        if (values == null) return Collections.emptyList();
+        return values.stream().filter(v -> "0".equals(v.getStatus()) && v.getDictValue() != null
+                && v.getDictValue().matches("[1-4]")).collect(Collectors.toList());
+    }
+
+    private void labels(List<AlarmVO> alarms) {
+        if (alarms.isEmpty()) return;
+        SysDictData query = new SysDictData();
+        query.setDictType("alarm_level");
+        Map<String, String> names = new HashMap<>();
+        List<SysDictData> values = dictDataService.selectDictDataList(query);
+        if (values != null) values.forEach(value -> names.putIfAbsent(value.getDictValue(), value.getDictLabel()));
+        alarms.forEach(alarm -> labels(alarm, names.get(String.valueOf(alarm.getAlarmLevel()))));
+    }
+
     private void labels(AlarmVO alarm) {
-        AlarmLevelEnum level = AlarmLevelEnum.fromCode(alarm.getAlarmLevel());
+        labels(alarm, alarm.getAlarmLevel() == null ? null : dictDataService.selectDictLabel("alarm_level", String.valueOf(alarm.getAlarmLevel())));
+    }
+
+    private void labels(AlarmVO alarm, String level) {
         AlarmAuditStatusEnum audit = AlarmAuditStatusEnum.fromCode(alarm.getAuditStatus());
-        alarm.setAlarmLevelName(level == null ? "未知等级" : level.getLabel());
+        alarm.setAlarmLevelName(level == null || level.isEmpty() ? "未知等级" : level);
         alarm.setAuditStatusName(audit == null ? "未审核" : audit.getLabel());
         alarm.setMonitorStatusName(Objects.equals(alarm.getAlarmStatus(), AlarmRecordStatusEnum.UNHANDLED.getCode()) ? "异常"
                 : Objects.equals(alarm.getAlarmStatus(), AlarmRecordStatusEnum.HANDLED.getCode()) ? "已恢复" : "未知");

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

@@ -8,6 +8,8 @@ import com.zksy.base.alarm.enums.*;
 import com.zksy.base.alarm.enums.AlarmLogOperTypeEnum;
 import com.zksy.common.core.domain.entity.SysUser;
 import com.zksy.system.mapper.SysUserMapper;
+import com.zksy.common.core.domain.entity.SysDictData;
+import com.zksy.system.service.ISysDictDataService;
 import com.zksy.ops.domain.WorkOrder;
 import com.zksy.base.alarm.mapper.*;
 import com.zksy.base.enums.EquipmentTypeEnum;
@@ -31,12 +33,13 @@ public class AlarmReviewService {
     private final AlarmReviewAttachmentService attachments;
     private final WorkOrderService workOrderService;
     private final SysUserMapper userMapper;
+    private final ISysDictDataService dictDataService;
 
     public Page<AlarmReviewVO> page(AlarmReviewQuery query) {
         validate(query);
         Page<AlarmReviewVO> page = reviewMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()),
                 query, scopeService.resolve(query), AlarmRecordStatusEnum.UNHANDLED.getCode());
-        page.getRecords().forEach(this::labels);
+        labels(page.getRecords());
         return page;
     }
 
@@ -113,8 +116,9 @@ public class AlarmReviewService {
         Map<String, Object> options = new LinkedHashMap<>();
         options.put("auditStatuses", Arrays.stream(AlarmAuditStatusEnum.values())
                 .map(value -> option(value.name(), value.getCode(), value.getLabel())).collect(Collectors.toList()));
-        options.put("alarmLevels", Arrays.stream(AlarmLevelEnum.values())
-                .map(value -> option(value.name(), value.getCode(), value.getLabel())).collect(Collectors.toList()));
+        options.put("alarmLevels", alarmLevels().stream()
+                .map(value -> option(AlarmLevelEnum.fromCode(Integer.valueOf(value.getDictValue())).name(),
+                        Integer.parseInt(value.getDictValue()), value.getDictLabel())).collect(Collectors.toList()));
         options.put("rootTypes", Arrays.stream(EquipmentTypeEnum.values()).filter(EquipmentTypeEnum::isRoot)
                 .map(value -> option(value.name(), value.getId(), value.getDesc())).collect(Collectors.toList()));
         return options;
@@ -129,7 +133,7 @@ public class AlarmReviewService {
     private void validate(AlarmReviewQuery query) {
         if (query.getPageNum() < 1 || query.getPageSize() < 1 || query.getPageSize() > 100)
             throw new ServiceException("分页参数无效");
-        if (query.getAlarmLevel() != null && AlarmLevelEnum.fromCode(query.getAlarmLevel()) == null)
+        if (query.getAlarmLevel() != null && alarmLevels().stream().noneMatch(v -> String.valueOf(query.getAlarmLevel()).equals(v.getDictValue())))
             throw new ServiceException("报警等级无效");
         if (query.getAuditStatus() != null && AlarmAuditStatusEnum.fromCode(query.getAuditStatus()) == null)
             throw new ServiceException("审核结果无效");
@@ -137,10 +141,33 @@ public class AlarmReviewService {
             throw new ServiceException("开始时间不能晚于结束时间");
     }
 
+    private List<SysDictData> alarmLevels() {
+        SysDictData query = new SysDictData();
+        query.setDictType("alarm_level");
+        query.setStatus("0");
+        List<SysDictData> values = dictDataService.selectDictDataList(query);
+        if (values == null) return Collections.emptyList();
+        return values.stream().filter(v -> "0".equals(v.getStatus()) && v.getDictValue() != null
+                && v.getDictValue().matches("[1-4]")).collect(Collectors.toList());
+    }
+
+    private void labels(List<AlarmReviewVO> alarms) {
+        if (alarms.isEmpty()) return;
+        SysDictData query = new SysDictData();
+        query.setDictType("alarm_level");
+        Map<String, String> names = new HashMap<>();
+        List<SysDictData> values = dictDataService.selectDictDataList(query);
+        if (values != null) values.forEach(value -> names.putIfAbsent(value.getDictValue(), value.getDictLabel()));
+        alarms.forEach(alarm -> labels(alarm, names.get(String.valueOf(alarm.getAlarmLevel()))));
+    }
+
     private void labels(AlarmReviewVO alarm) {
-        AlarmLevelEnum level = AlarmLevelEnum.fromCode(alarm.getAlarmLevel());
+        labels(alarm, alarm.getAlarmLevel() == null ? null : dictDataService.selectDictLabel("alarm_level", String.valueOf(alarm.getAlarmLevel())));
+    }
+
+    private void labels(AlarmReviewVO alarm, String level) {
         AlarmAuditStatusEnum audit = AlarmAuditStatusEnum.fromCode(alarm.getAuditStatus());
-        alarm.setAlarmLevelName(level == null ? "未知等级" : level.getLabel());
+        alarm.setAlarmLevelName(level == null || level.isEmpty() ? "未知等级" : level);
         alarm.setAuditStatusName(audit == null ? "未知结果" : audit.getLabel());
     }
 }

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/MaintainOrderVO.java

@@ -32,7 +32,7 @@ public class MaintainOrderVO {
     @ApiModelProperty("单一接单责任人用户ID")
     private Long receiveUserId;
 
-    @ApiModelProperty("工单优先级:1-紧急 2-一般 3-低")
+    @ApiModelProperty("工单优先级:work_order_level 字典整数值,初始0紧急/1一般/2低")
     private Integer orderLevel;
 
     @ApiModelProperty("维护人")

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

@@ -37,7 +37,7 @@ public class WorkOrder implements Serializable {
     @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date faultTime;
 
-    @ApiModelProperty(value = "工单优先级:0-紧急 1-一般 2-低")
+    @ApiModelProperty(value = "工单优先级:work_order_level 字典整数值,初始0紧急/1一般/2低")
     private Integer orderLevel;
 
     @ApiModelProperty(value = "工单问题描述")

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/domain/vo/WorkOrderPushVO.java

@@ -30,7 +30,7 @@ public class WorkOrderPushVO implements Serializable {
     @ApiModelProperty("工单状态描述")
     private String orderStatusDesc;
 
-    @ApiModelProperty("工单优先级:1-紧急 2-一般 3-低")
+    @ApiModelProperty("工单优先级:work_order_level 字典整数值,初始0紧急/1一般/2低")
     private Integer orderLevel;
 
     @ApiModelProperty("所属部门ID")

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/domain/vo/WorkOrderVO.java

@@ -43,7 +43,7 @@ public class WorkOrderVO implements Serializable {
     @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date faultTime;
 
-    @ApiModelProperty("工单优先级:0-紧急 1-一般 2-低")
+    @ApiModelProperty("工单优先级:work_order_level 字典整数值,初始0紧急/1一般/2低")
     private Integer orderLevel;
 
     @ApiModelProperty("工单问题描述")

+ 2 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/ops/dto/WorkOrderAddDTO.java

@@ -5,8 +5,6 @@ import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
-import javax.validation.constraints.Max;
-import javax.validation.constraints.Min;
 import javax.validation.constraints.NotBlank;
 import java.io.Serializable;
 import java.util.Date;
@@ -35,10 +33,8 @@ public class WorkOrderAddDTO implements Serializable {
     @ApiModelProperty(value = "设备ID,传入后由后端回填设备编码")
     private String deviceId;
 
-    /** 工单优先级:0-紧急 1-一般 2-低,不传默认 1-一般 */
-    @Min(value = 0, message = "工单优先级取值为 0-紧急、1-一般、2-低")
-    @Max(value = 2, message = "工单优先级取值为 0-紧急、1-一般、2-低")
-    @ApiModelProperty(value = "工单优先级:0-紧急 1-一般 2-低,不传默认 1-一般")
+    /** 工单优先级由 work_order_level 字典校验,未传取启用默认项。 */
+    @ApiModelProperty(value = "工单优先级:work_order_level 字典启用整数值,初始0紧急/1一般/2低;不传取启用默认项,无默认项时报错")
     private Integer orderLevel;
 
     /** 故障发生时间,不传取当前时间 */

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

@@ -25,6 +25,11 @@ public class WorkOrderDispatchDTO implements Serializable {
     @ApiModelProperty(value = "责任人用户ID", required = true)
     private Long handlerId;
 
+    /** 核验人用户ID */
+    @NotNull(message = "核验人不能为空")
+    @ApiModelProperty(value = "核验人用户ID", required = true)
+    private Long verifyUser;
+
     /** 派单备注 */
     @ApiModelProperty(value = "派单备注")
     private String remarks;

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

@@ -30,8 +30,8 @@ public class WorkOrderPageDTO extends BaseEntity {
     @ApiModelProperty(value = "每页条数")
     private Long pageSize = 10L;
 
-    /** 工单优先级:1-紧急 2-一般 3-低 */
-    @ApiModelProperty(value = "工单优先级:1-紧急 2-一般 3-低")
+    /** 工单优先级:work_order_level 字典整数值,初始0紧急/1一般/2低 */
+    @ApiModelProperty(value = "工单优先级:work_order_level 字典整数值,初始0紧急/1一般/2低")
     private Integer orderLevel;
 
     /** 工单状态:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消,支持逗号分隔多值 */

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

@@ -1,60 +0,0 @@
-package com.zksy.ops.enums;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.util.Objects;
-
-/**
- * 工单优先级枚举(work_order.order_level)
- *
- * @author zengy
- */
-@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));
-    }
-}

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

@@ -16,7 +16,8 @@ import com.zksy.ops.domain.vo.WorkOrderLogVO;
 import com.zksy.ops.domain.vo.WorkOrderPushVO;
 import com.zksy.ops.domain.vo.WorkOrderVO;
 import com.zksy.ops.enums.WorkOrderDelayApplyStatusEnum;
-import com.zksy.ops.enums.WorkOrderLevelEnum;
+import com.zksy.common.core.domain.entity.SysDictData;
+import com.zksy.system.service.ISysDictDataService;
 import com.zksy.ops.enums.WorkOrderLogOperTypeEnum;
 import com.zksy.ops.enums.WorkOrderStatusEnum;
 import com.zksy.base.mapper.EquipmentBaseMapper;
@@ -47,6 +48,7 @@ import com.zksy.ops.support.OpsAssignScopeSupport;
 import com.zksy.system.service.ISysUserService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
@@ -109,6 +111,12 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
      */
     private static final String DATA_SCOPE = "dataScope";
 
+    @Value("${minio.endpoint}")
+    private String minioEndpoint;
+
+    @Value("${minio.bucket}")
+    private String minioBucket;
+
     @Autowired
     private WorkOrderLogMapper workOrderLogMapper;
 
@@ -134,6 +142,11 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     @Autowired
     private ISysUserService sysUserService;
 
+    @Autowired
+    private ISysDictDataService sysDictDataService;
+
+    private static final String WORK_ORDER_LEVEL_DICT = "work_order_level";
+
     @Autowired
     private OpsAssignScopeSupport assignScopeSupport;
 
@@ -272,9 +285,11 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
             throw new ServiceException("故障描述不能为空");
         }
         Date now = new Date();
+        workOrder.setMaintenanceBeforeImg(normalizeAttachmentPaths(workOrder.getMaintenanceBeforeImg()));
+        workOrder.setMaintenanceAfterImg(normalizeAttachmentPaths(workOrder.getMaintenanceAfterImg()));
         workOrder.setOrderId(null);
         workOrder.setOrderNo(generateOrderNo());
-        workOrder.setOrderLevel(WorkOrderLevelEnum.getByCode(workOrder.getOrderLevel()).getCode());
+        workOrder.setOrderLevel(normalizeWorkOrderLevel(workOrder.getOrderLevel()));
         if (workOrder.getFaultTime() == null) {
             workOrder.setFaultTime(now);
         }
@@ -338,9 +353,8 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         WorkOrder workOrder = new WorkOrder();
         workOrder.setOrderNo(generateOrderNo());
         workOrder.setDeviceId(deviceId);
-        workOrder.setOrderLevel(orderLevel == null
-                ? mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()).getCode()
-                : WorkOrderLevelEnum.getByCode(orderLevel).getCode());
+        workOrder.setOrderLevel(normalizeWorkOrderLevel(orderLevel == null
+                ? mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()) : orderLevel));
         workOrder.setOrderDesc(StringUtils.isNotEmpty(orderDesc) ? orderDesc : buildAlarmOrderDesc(alarm));
         workOrder.setFaultTime(alarm.getCreateTime() == null
                 ? now
@@ -390,7 +404,7 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         WorkOrder workOrder = new WorkOrder();
         workOrder.setOrderNo(generateOrderNo());
         workOrder.setDeviceId(equipmentId);
-        workOrder.setOrderLevel(mapHazardLevelToOrderLevel(hazardLevel).getCode());
+        workOrder.setOrderLevel(normalizeWorkOrderLevel(mapHazardLevelToOrderLevel(hazardLevel)));
         workOrder.setOrderDesc(hazardDesc);
         // 数据按部门隔离:默认归属创建人所在部门(框架 DataScopeAspect 依据该字段过滤)
         workOrder.setDeptId(assignScopeSupport.currentDeptId());
@@ -398,7 +412,7 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         workOrder.setDelayApplyStatus(WorkOrderDelayApplyStatusEnum.NONE.getCode());
         workOrder.setPlanFinishTime(planFinishTime);
         if (StringUtils.isNotEmpty(hazardImages)) {
-            workOrder.setMaintenanceBeforeImg(hazardImages);
+            workOrder.setMaintenanceBeforeImg(normalizeAttachmentPaths(hazardImages));
         }
         workOrder.setFaultTime(now);
         workOrder.setCreateTime(now);
@@ -423,6 +437,10 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         if (handlerId == null) {
             throw new ServiceException("责任人不能为空");
         }
+        Long verifyUserId = dto.getVerifyUser();
+        if (verifyUserId == null) {
+            throw new ServiceException("核验人不能为空");
+        }
         WorkOrder order = getOrderById(id);
         if (!Objects.equals(WorkOrderStatusEnum.PENDING_DISPATCH.getCode(), order.getOrderStatus())) {
             throw new ServiceException("仅待派单工单允许派单");
@@ -432,16 +450,22 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         if (handler == null || !UserConstants.NORMAL.equals(String.valueOf(handler.getStatus())) || !UserConstants.NORMAL.equals(String.valueOf(handler.getDelFlag()))) {
             throw new ServiceException("责任人不存在或已停用");
         }
+        assignScopeSupport.validateAssignUserInScope(verifyUserId);
+        SysUser verifier = sysUserService.selectUserById(verifyUserId);
+        if (verifier == null || !UserConstants.NORMAL.equals(String.valueOf(verifier.getStatus())) || !UserConstants.NORMAL.equals(String.valueOf(verifier.getDelFlag()))) {
+            throw new ServiceException("核验人不存在或已停用");
+        }
 
         Date now = new Date();
-        this.update(updateWrapper(id).set(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_ACCEPT.getCode()).set(WorkOrder::getReceiveUser, handlerId).set(WorkOrder::getDispatchUser, currentUserId()).set(WorkOrder::getDispatchTime, now)
+        this.update(updateWrapper(id).set(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_ACCEPT.getCode()).set(WorkOrder::getReceiveUser, handlerId).set(WorkOrder::getVerifyUser, verifyUserId).set(WorkOrder::getDispatchUser, currentUserId()).set(WorkOrder::getDispatchTime, now)
                 // 数据归属部门跟随派单指定的责任人
                 .set(handler.getDeptId() != null, WorkOrder::getDeptId, handler.getDeptId())
                 // 计划完成时间:派单时可设定/修改,为空则保留原值
                 .set(dto.getPlanFinishTime() != null, WorkOrder::getPlanFinishTime, dto.getPlanFinishTime())
                 .eq(WorkOrder::getOrderStatus, WorkOrderStatusEnum.PENDING_DISPATCH.getCode()));
 
-        String logContent = "派单给责任人:" + userLabel(handler) + (StringUtils.isEmpty(dto.getRemarks()) ? "" : ",备注:" + dto.getRemarks());
+        String logContent = "派单给责任人:" + userLabel(handler) + ",核验人:" + userLabel(verifier)
+                + (StringUtils.isEmpty(dto.getRemarks()) ? "" : ",备注:" + dto.getRemarks());
         writeLog(id, WorkOrderLogOperTypeEnum.DISPATCH, logContent, null);
         return true;
     }
@@ -453,14 +477,15 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         requireResponsible(order, currentUserId(), "核查接单");
         requireStatus(order, WorkOrderStatusEnum.PENDING_ACCEPT, "仅待接单工单允许核查接单");
         // 核查接单必须上传现场情况附件
-        if (StringUtils.isEmpty(dto.getAttachUrl())) {
+        String attachUrl = normalizeAttachmentPaths(dto.getAttachUrl());
+        if (StringUtils.isEmpty(attachUrl)) {
             throw new ServiceException("核查接单必须上传现场情况附件");
         }
 
         Long id = order.getOrderId();
         // 维修前图片固定来源于核查接单上传的现场情况附件
-        boolean updated = transition(id, WorkOrderStatusEnum.PENDING_ACCEPT.getCode(), WorkOrderStatusEnum.ACCEPTED.getCode(), wrapper -> wrapper.set(WorkOrder::getMaintenanceBeforeImg, dto.getAttachUrl()));
-        writeLog(id, WorkOrderLogOperTypeEnum.ACCEPT, buildDesc("", dto.getContent()), dto.getAttachUrl());
+        boolean updated = transition(id, WorkOrderStatusEnum.PENDING_ACCEPT.getCode(), WorkOrderStatusEnum.ACCEPTED.getCode(), wrapper -> wrapper.set(WorkOrder::getMaintenanceBeforeImg, attachUrl));
+        writeLog(id, WorkOrderLogOperTypeEnum.ACCEPT, buildDesc("", dto.getContent()), attachUrl);
         return updated;
     }
 
@@ -496,13 +521,14 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
             throw new ServiceException("仅处理中/延期中工单允许提交验收");
         }
         // 提交验收必须上传维修后的情况附件
-        if (StringUtils.isEmpty(dto.getAttachUrl())) {
+        String attachUrl = normalizeAttachmentPaths(dto.getAttachUrl());
+        if (StringUtils.isEmpty(attachUrl)) {
             throw new ServiceException("提交验收必须上传维修后的情况附件");
         }
 
         // 附件已在上方强制必填,直接写入工单维修后图片
-        boolean updated = transition(order.getOrderId(), status, WorkOrderStatusEnum.PENDING_VERIFY.getCode(), wrapper -> wrapper.set(WorkOrder::getMaintenanceAfterImg, dto.getAttachUrl()));
-        writeLog(order.getOrderId(), WorkOrderLogOperTypeEnum.SUBMIT_VERIFY, buildDesc("", dto.getContent()), dto.getAttachUrl());
+        boolean updated = transition(order.getOrderId(), status, WorkOrderStatusEnum.PENDING_VERIFY.getCode(), wrapper -> wrapper.set(WorkOrder::getMaintenanceAfterImg, attachUrl));
+        writeLog(order.getOrderId(), WorkOrderLogOperTypeEnum.SUBMIT_VERIFY, buildDesc("", dto.getContent()), attachUrl);
         return updated;
     }
 
@@ -835,6 +861,43 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         }
     }
 
+    String normalizeAttachmentPaths(String value) {
+        if (value == null) {
+            return null;
+        }
+        return Arrays.stream(value.split(","))
+                .map(String::trim)
+                .filter(path -> !path.isEmpty())
+                .map(this::normalizeAttachmentPath)
+                .collect(Collectors.joining(","));
+    }
+
+    private String normalizeAttachmentPath(String value) {
+        String path = value;
+        String base = StringUtils.isBlank(minioEndpoint) || StringUtils.isBlank(minioBucket)
+                ? "" : minioEndpoint.replaceAll("/+$", "") + "/" + minioBucket + "/";
+        if (!base.isEmpty() && path.startsWith(base)) {
+            path = path.substring(base.length());
+        }
+        try {
+            java.net.URI uri = java.net.URI.create(path);
+            String decodedPath = uri.getPath();
+            if (uri.isAbsolute() || uri.getRawAuthority() != null || uri.getRawQuery() != null
+                    || uri.getRawFragment() != null || decodedPath == null
+                    || decodedPath.contains("\\") || decodedPath.contains("//")
+                    || Arrays.stream(decodedPath.split("/")).anyMatch(part -> ".".equals(part) || "..".equals(part))) {
+                throw new IllegalArgumentException();
+            }
+            path = "/" + path.replaceFirst("^/+", "");
+            if ("/".equals(path)) {
+                throw new IllegalArgumentException();
+            }
+            return path;
+        } catch (IllegalArgumentException ex) {
+            throw new ServiceException("工单附件必须使用有效的 MinIO 相对路径");
+        }
+    }
+
     /**
      * 写入工单操作日志(操作人取当前登录用户)
      */
@@ -843,7 +906,7 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         logEntry.setWorkOrderId(orderId);
         logEntry.setOperType(operType.getCode());
         logEntry.setOperDesc(operDesc);
-        logEntry.setAttachUrl(attachUrl);
+        logEntry.setAttachUrl(normalizeAttachmentPaths(attachUrl));
         logEntry.setOperUserId(currentUserId());
         logEntry.setOperUserName(currentUserName());
         logEntry.setOperTime(new Date());
@@ -869,25 +932,41 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     /**
      * 隐患等级映射工单优先级:0-重大→紧急 3-轻微→低 其余→一般
      */
-    private WorkOrderLevelEnum mapHazardLevelToOrderLevel(String hazardLevel) {
-        if ("0".equals(hazardLevel)) {
-            return WorkOrderLevelEnum.URGENT;
-        }
-        if ("3".equals(hazardLevel)) {
-            return WorkOrderLevelEnum.LOW;
-        }
-        return WorkOrderLevelEnum.NORMAL;
+    private Integer mapHazardLevelToOrderLevel(String hazardLevel) {
+        if ("0".equals(hazardLevel)) return 0;
+        if ("3".equals(hazardLevel)) return 2;
+        return 1;
     }
 
     /** 报警等级映射工单优先级:1级最高为紧急,4级最低为低,其余为一般。 */
-    private WorkOrderLevelEnum mapAlarmLevelToOrderLevel(Integer alarmLevel) {
-        if (alarmLevel == null || alarmLevel == 1) {
-            return WorkOrderLevelEnum.URGENT;
-        }
-        if (alarmLevel == 4) {
-            return WorkOrderLevelEnum.LOW;
+    private Integer mapAlarmLevelToOrderLevel(Integer alarmLevel) {
+        if (alarmLevel == null || alarmLevel == 1) return 0;
+        if (alarmLevel == 4) return 2;
+        return 1;
+    }
+
+    private Integer normalizeWorkOrderLevel(Integer value) {
+        SysDictData query = new SysDictData();
+        query.setDictType(WORK_ORDER_LEVEL_DICT);
+        query.setStatus("0");
+        List<SysDictData> values = sysDictDataService.selectDictDataList(query);
+        if (values == null) values = Collections.emptyList();
+        if (value == null) {
+            for (SysDictData item : values) {
+                if ("0".equals(item.getStatus()) && "Y".equals(item.getIsDefault())) {
+                    try {
+                        return Integer.valueOf(item.getDictValue());
+                    } catch (NumberFormatException ignored) {
+                        throw new ServiceException("工单优先级默认字典值必须为整数");
+                    }
+                }
+            }
+            throw new ServiceException("请配置启用的工单优先级默认字典项");
         }
-        return WorkOrderLevelEnum.NORMAL;
+        boolean valid = values.stream().anyMatch(item -> "0".equals(item.getStatus())
+                && String.valueOf(value).equals(item.getDictValue()));
+        if (!valid) throw new ServiceException("工单优先级无效");
+        return value;
     }
 
     /** 生成报警转工单时的默认问题描述。 */

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

@@ -66,6 +66,12 @@ class WaterSupplyAlarmRealtimeContractTest {
     @BeforeEach
     void setUp() {
         service = new WaterSupplyAlarmServiceImpl();
+        com.zksy.system.service.ISysDictDataService dictionaries = mock(com.zksy.system.service.ISysDictDataService.class);
+        ReflectionTestUtils.setField(service, "dictDataService", dictionaries);
+        when(dictionaries.selectDictLabel(any(), any())).thenAnswer(call -> {
+            com.zksy.base.alarm.enums.AlarmLevelEnum level = com.zksy.base.alarm.enums.AlarmLevelEnum.fromCode(Integer.valueOf(call.getArgument(1)));
+            return level == null ? null : level.getLabel();
+        });
         alarmDataService = mock(AlarmDataService.class);
         alarmDataMapper = mock(AlarmDataMapper.class);
         equipmentBaseService = mock(EquipmentBaseService.class);

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

@@ -18,10 +18,19 @@ class AlarmOperationsServiceTest {
     private final AlarmOperationsMapper mapper = mock(AlarmOperationsMapper.class);
     private final AlarmReviewScopeService scope = mock(AlarmReviewScopeService.class);
     private final AlarmLogMapper logs = mock(AlarmLogMapper.class);
-    private final AlarmOperationsService service = new AlarmOperationsService(mapper, scope, logs);
+    private final com.zksy.system.service.ISysDictDataService dictionaries = mock(com.zksy.system.service.ISysDictDataService.class);
+    private final AlarmOperationsService service = new AlarmOperationsService(mapper, scope, logs, dictionaries);
     private final AlarmQuery query = new AlarmQuery();
     private final AlarmVO alarm = new AlarmVO();
     @BeforeEach void setup() {
+        List<com.zksy.common.core.domain.entity.SysDictData> levels = new ArrayList<>();
+        for (AlarmLevelEnum level : AlarmLevelEnum.values()) {
+            com.zksy.common.core.domain.entity.SysDictData data = new com.zksy.common.core.domain.entity.SysDictData();
+            data.setDictType("alarm_level"); data.setDictValue(String.valueOf(level.getCode()));
+            data.setDictLabel(level.getLabel()); data.setStatus("0"); levels.add(data);
+            when(dictionaries.selectDictLabel("alarm_level", data.getDictValue())).thenReturn(data.getDictLabel());
+        }
+        when(dictionaries.selectDictDataList(any())).thenReturn(levels);
         query.setRootTypeId(2);
         alarm.setId("a1"); alarm.setDeviceCode("D1"); alarm.setWarningCode("WARN_INSTANT_FLOW");
         alarm.setCreateTime(LocalDateTime.of(2026,1,10,12,0));
@@ -30,6 +39,18 @@ class AlarmOperationsServiceTest {
         when(scope.resolveOperations(query)).thenReturn(Arrays.asList(2,21));
         when(mapper.selectDetail(eq("a1"),eq(query),anyList())).thenReturn(alarm);
     }
+    @Test void alarmDictionaryControlsOptionsLabelsAndEnabledFilters() {
+        com.zksy.common.core.domain.entity.SysDictData level = new com.zksy.common.core.domain.entity.SysDictData();
+        level.setDictValue("2"); level.setDictLabel("管理员配置报警"); level.setStatus("0");
+        when(dictionaries.selectDictDataList(any())).thenReturn(Collections.singletonList(level));
+        when(dictionaries.selectDictLabel("alarm_level", "2")).thenReturn(level.getDictLabel());
+        assertEquals(Collections.singletonList(Map.of("value", 2, "label", "管理员配置报警")), service.options(query).get("alarmLevels"));
+        assertEquals("管理员配置报警", service.detail("a1", query).getAlarmLevelName());
+        level.setStatus("1");
+        assertEquals(Collections.emptyList(), service.options(query).get("alarmLevels"));
+        query.setAlarmLevel(2);
+        assertThrows(ServiceException.class, () -> service.statistics(query));
+    }
     @Test void ledgerUsesServerPaginationAndKeepsRecoveredEvents() {
         query.setPageNum(3); query.setPageSize(20);
         Page<AlarmVO> page = new Page<>(3,20,41); page.setRecords(Collections.singletonList(alarm));

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

@@ -36,7 +36,8 @@ class AlarmReviewServiceTest {
     private final AlarmReviewAttachmentService attachments = mock(AlarmReviewAttachmentService.class);
     private final WorkOrderService orders = mock(WorkOrderService.class);
     private final SysUserMapper users = mock(SysUserMapper.class);
-    private final AlarmReviewService service = new AlarmReviewService(queries, alarms, logs, scope, attachments, orders, users);
+    private final com.zksy.system.service.ISysDictDataService dictionaries = mock(com.zksy.system.service.ISysDictDataService.class);
+    private final AlarmReviewService service = new AlarmReviewService(queries, alarms, logs, scope, attachments, orders, users, dictionaries);
     private final AlarmReviewQuery query = new AlarmReviewQuery();
     private AlarmReviewVO alarm;
 
@@ -46,6 +47,14 @@ class AlarmReviewServiceTest {
         TableInfoHelper.initTableInfo(assistant, AlarmLog.class);
     }
     @BeforeEach void setup() {
+        List<com.zksy.common.core.domain.entity.SysDictData> levels = new ArrayList<>();
+        for (AlarmLevelEnum level : AlarmLevelEnum.values()) {
+            com.zksy.common.core.domain.entity.SysDictData data = new com.zksy.common.core.domain.entity.SysDictData();
+            data.setDictType("alarm_level"); data.setDictValue(String.valueOf(level.getCode()));
+            data.setDictLabel(level.getLabel()); data.setStatus("0"); levels.add(data);
+            when(dictionaries.selectDictLabel("alarm_level", data.getDictValue())).thenReturn(data.getDictLabel());
+        }
+        when(dictionaries.selectDictDataList(any())).thenReturn(levels);
         SysUser user = new SysUser(); user.setUserName("audit-account"); user.setNickName("审核员昵称");
         when(users.selectUserById(9L)).thenReturn(user);
         WorkOrder order = new WorkOrder(); order.setOrderId(88L); order.setOrderNo("WO202609241234560001");

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

@@ -65,6 +65,8 @@ class WorkOrderServiceImplTest {
 
     private static final Long HANDLER_ID = 88L;
 
+    private static final Long VERIFIER_ID = 89L;
+
     private WorkOrderServiceImpl service;
 
     private WorkOrderMapper orderMapper;
@@ -102,6 +104,15 @@ class WorkOrderServiceImplTest {
     @BeforeEach
     void setUp() {
         service = new WorkOrderServiceImpl();
+        com.zksy.system.service.ISysDictDataService dictionaries = mock(com.zksy.system.service.ISysDictDataService.class);
+        java.util.List<com.zksy.common.core.domain.entity.SysDictData> levels = new java.util.ArrayList<>();
+        for (int value = 0; value < 3; value++) {
+            com.zksy.common.core.domain.entity.SysDictData data = new com.zksy.common.core.domain.entity.SysDictData();
+            data.setDictType("work_order_level"); data.setDictValue(String.valueOf(value));
+            data.setStatus("0"); data.setIsDefault(value == 1 ? "Y" : "N"); levels.add(data);
+        }
+        when(dictionaries.selectDictDataList(any())).thenReturn(levels);
+        ReflectionTestUtils.setField(service, "sysDictDataService", dictionaries);
         orderMapper = mock(WorkOrderMapper.class);
         logMapper = mock(WorkOrderLogMapper.class);
         when(logMapper.insert(any(WorkOrderLog.class))).thenReturn(1);
@@ -124,6 +135,23 @@ class WorkOrderServiceImplTest {
         ReflectionTestUtils.setField(service, "assignScopeSupport", mock(OpsAssignScopeSupport.class));
     }
 
+    @Test
+    void manualOrderUsesConfiguredDefaultAndRejectsDisabledLevels() {
+        com.zksy.system.service.ISysDictDataService dictionaries =
+                (com.zksy.system.service.ISysDictDataService) ReflectionTestUtils.getField(service, "sysDictDataService");
+        com.zksy.common.core.domain.entity.SysDictData configured = new com.zksy.common.core.domain.entity.SysDictData();
+        configured.setDictValue("2"); configured.setStatus("0"); configured.setIsDefault("Y");
+        when(dictionaries.selectDictDataList(any())).thenReturn(Collections.singletonList(configured));
+        WorkOrder order = new WorkOrder(); order.setOrderDesc("故障");
+        service.createWorkOrder(order);
+        assertEquals(2, order.getOrderLevel());
+        configured.setStatus("1");
+        WorkOrder disabled = new WorkOrder(); disabled.setOrderDesc("故障"); disabled.setOrderLevel(2);
+        assertThrows(ServiceException.class, () -> service.createWorkOrder(disabled));
+        WorkOrder missingDefault = new WorkOrder(); missingDefault.setOrderDesc("故障");
+        assertThrows(ServiceException.class, () -> service.createWorkOrder(missingDefault));
+    }
+
     @Test
     void alarmConfirmationLinksNewOrderWithoutHandlingAlarmOrClearingDevice() {
         AlarmData alarm = new AlarmData();
@@ -193,6 +221,38 @@ class WorkOrderServiceImplTest {
                 .anyMatch(field -> field.getType().getSimpleName().equals("AlarmLogMapper")));
     }
 
+    @Test
+    void attachmentPathsAreRelativeAndIdempotent() {
+        ReflectionTestUtils.setField(service, "minioEndpoint", "https://files.example.test/");
+        ReflectionTestUtils.setField(service, "minioBucket", "attachments");
+        String path = "/2026/10/09/workOrder/jpg/4649512327058833952.jpg";
+        String expected = path + ",/2026/10/09/upload/jpg/old.jpg";
+        assertEquals(expected, service.normalizeAttachmentPaths(" https://files.example.test/attachments" + path
+                + ",,2026/10/09/upload/jpg/old.jpg, "));
+        assertEquals(expected, service.normalizeAttachmentPaths(expected));
+        assertNull(service.normalizeAttachmentPaths(null));
+        assertEquals("", service.normalizeAttachmentPaths(" , , "));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"https://other.example.test/a.jpg", "blob:abc", "data:image/png;base64,abc",
+            "//other.example.test/a.jpg", "/../a.jpg", "/%2e%2e/a.jpg", "/a.jpg?token=abc", "/"})
+    void invalidAttachmentPathsAreRejected(String path) {
+        assertThrows(ServiceException.class, () -> service.normalizeAttachmentPaths(path));
+    }
+
+    @Test
+    void manualCreationNormalizesBothImageFields() {
+        WorkOrder order = new WorkOrder();
+        order.setOrderDesc("附件路径测试");
+        order.setMaintenanceBeforeImg("2026/10/09/workOrder/jpg/before.jpg");
+        order.setMaintenanceAfterImg("/2026/10/09/workOrder/jpg/after.jpg");
+        stubOrderInsert(101L);
+        service.createWorkOrder(order);
+        assertEquals("/2026/10/09/workOrder/jpg/before.jpg", order.getMaintenanceBeforeImg());
+        assertEquals("/2026/10/09/workOrder/jpg/after.jpg", order.getMaintenanceAfterImg());
+    }
+
     @Test
     void manualCreationDoesNotQueryDeviceOrAlarmConflicts() {
         WorkOrder order = new WorkOrder();
@@ -218,6 +278,28 @@ class WorkOrderServiceImplTest {
         org.mockito.Mockito.verifyNoInteractions(equipmentBaseMapper, alarmDataService);
     }
 
+    @Test
+    void automaticMappingsRejectDisabledLevelWithoutCreatingOrder() {
+        com.zksy.system.service.ISysDictDataService dictionaries =
+                (com.zksy.system.service.ISysDictDataService) ReflectionTestUtils.getField(service, "sysDictDataService");
+        com.zksy.common.core.domain.entity.SysDictData disabled = new com.zksy.common.core.domain.entity.SysDictData();
+        disabled.setDictValue("0"); disabled.setStatus("1");
+        when(dictionaries.selectDictDataList(any())).thenReturn(Collections.singletonList(disabled));
+        stubAlarmForConflict("WARN-PRESSURE");
+        ServiceException alarmError = assertThrows(ServiceException.class,
+                () -> service.createWorkOrderFromAlarm("a-conflict", null, null));
+        assertEquals("工单优先级无效", alarmError.getMessage());
+        HazardHiddenAccount hazard = new HazardHiddenAccount(); hazard.setHazardId("h-disabled");
+        when(hazardMapper.selectById("h-disabled")).thenReturn(hazard);
+        ServiceException hazardError = assertThrows(ServiceException.class,
+                () -> service.createWorkOrderFromHazard("h-disabled", "H-DISABLED", "隐患", "eq-conflict", "0", null, null));
+        assertEquals("工单优先级无效", hazardError.getMessage());
+        verify(orderMapper, never()).insert(any(WorkOrder.class));
+        verify(alarmDataService, never()).update(any(AlarmData.class), any());
+        verify(hazardMapper, never()).update(any(HazardHiddenAccount.class), any());
+        org.mockito.Mockito.verifyNoInteractions(logMapper, caseInfoMapper);
+    }
+
     @Test
     void sameDeviceAndWarningCodeConflictIsRejectedBeforeWrites() {
         stubAlarmForConflict("WARN-PRESSURE");
@@ -313,6 +395,18 @@ class WorkOrderServiceImplTest {
         verify(orderMapper, never()).selectById(any());
     }
 
+    @Test
+    void dispatchRejectsMissingVerifierId() {
+        WorkOrderDispatchDTO dto = new WorkOrderDispatchDTO();
+        dto.setHandlerId(HANDLER_ID);
+
+        ServiceException error = assertThrows(ServiceException.class,
+                () -> service.dispatch(ORDER_ID, dto));
+
+        assertEquals("核验人不能为空", error.getMessage());
+        verify(orderMapper, never()).selectById(any());
+    }
+
     @Test
     void dispatchRejectsOrderNotFound() {
         when(orderMapper.selectById(ORDER_ID)).thenReturn(null);
@@ -351,15 +445,40 @@ class WorkOrderServiceImplTest {
         verify(orderMapper, never()).update(isNull(), any());
     }
 
+    @Test
+    void dispatchRejectsInactiveVerifierWithoutUpdatingOrder() {
+        when(orderMapper.selectById(ORDER_ID)).thenReturn(pendingOrder());
+        when(sysUserService.selectUserById(HANDLER_ID)).thenReturn(activeReceiver());
+        SysUser disabled = new SysUser();
+        disabled.setUserId(VERIFIER_ID);
+        disabled.setStatus("1");
+        disabled.setDelFlag("0");
+        when(sysUserService.selectUserById(VERIFIER_ID)).thenReturn(disabled);
+
+        ServiceException error = assertThrows(ServiceException.class,
+                () -> service.dispatch(ORDER_ID, dispatchDTO()));
+
+        assertEquals("核验人不存在或已停用", error.getMessage());
+        verify(orderMapper, never()).update(isNull(), any());
+    }
+
     @Test
     void dispatchMovesPendingDispatchOrderToPendingAccept() {
         when(orderMapper.selectById(ORDER_ID)).thenReturn(pendingOrder());
         when(sysUserService.selectUserById(HANDLER_ID)).thenReturn(activeReceiver());
+        when(sysUserService.selectUserById(VERIFIER_ID)).thenReturn(activeVerifier());
         when(orderMapper.update(isNull(), any())).thenReturn(1);
 
         assertTrue(service.dispatch(ORDER_ID, dispatchDTO()));
 
-        verify(orderMapper).update(isNull(), any());
+        org.mockito.ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper<WorkOrder>> updateCaptor =
+                org.mockito.ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
+        verify(orderMapper).update(isNull(), updateCaptor.capture());
+        com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WorkOrder> updateWrapper =
+                (com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WorkOrder>) updateCaptor.getValue();
+        assertTrue(updateWrapper.getSqlSet().contains("verify_user"));
+        assertTrue(updateWrapper.getParamNameValuePairs().containsValue(VERIFIER_ID));
+        verify(sysUserService).selectUserById(VERIFIER_ID);
         verify(logMapper).insert(any(WorkOrderLog.class));
     }
 
@@ -421,9 +540,21 @@ class WorkOrderServiceImplTest {
         return user;
     }
 
+    private SysUser activeVerifier() {
+        SysUser user = new SysUser();
+        user.setUserId(VERIFIER_ID);
+        user.setUserName("verifier");
+        user.setNickName("核验员");
+        user.setDeptId(7L);
+        user.setStatus("0");
+        user.setDelFlag("0");
+        return user;
+    }
+
     private WorkOrderDispatchDTO dispatchDTO() {
         WorkOrderDispatchDTO dto = new WorkOrderDispatchDTO();
         dto.setHandlerId(HANDLER_ID);
+        dto.setVerifyUser(VERIFIER_ID);
         dto.setRemarks("请尽快处理");
         return dto;
     }

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

@@ -52,7 +52,7 @@ public class AlarmData implements Serializable {
     private BigDecimal actualValue;
 
     @TableField(value = "alarm_level")
-    @ApiModelProperty(value = "告警等级 1-4,1级最高")
+    @ApiModelProperty(value = "报警等级:alarm_level 字典,保留1-4计算编码,1级最高,名称与样式由字典配置")
     private Integer alarmLevel;
 
     @TableField(value = "deviation_ratio")

+ 1 - 1
zk-api-service/src/main/java/com/zksy/api/domain/WorkOrder.java

@@ -36,7 +36,7 @@ public class WorkOrder implements Serializable {
     @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date faultTime;
 
-    @ApiModelProperty(value = "工单优先级:0-紧急 1-一般 2-低")
+    @ApiModelProperty(value = "工单优先级:work_order_level 字典整数值,初始0紧急/1一般/2低")
     private Integer orderLevel;
 
     @ApiModelProperty(value = "工单问题描述")

+ 5 - 23
zk-api-service/src/main/java/com/zksy/api/domain/WorkOrderLog.java

@@ -24,47 +24,29 @@ public class WorkOrderLog implements Serializable {
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;
 
-    /**
-     * 日志ID
-     */
     @ApiModelProperty(value = "日志ID")
     @TableId(type = IdType.AUTO)
     private Long logId;
 
-    /**
-     * 关联工单ID
-     */
     @ApiModelProperty(value = "工单ID")
     private Long workOrderId;
 
-    /**
-     * 操作类型:1-创建工单 2-派单 3-接单 4-开始维修 5-提交验收 6-验收通过 7-驳回工单 8-申请延期
-     */
-    @ApiModelProperty(value = "操作类型:1-创建工单 2-派单 3-接单 4-开始维修 5-提交验收 6-验收通过 7-驳回工单 8-申请延期 9-延期审核通过")
+    @ApiModelProperty(value = "操作类型:0-创建工单 1-派单 2-接单 3-处置 4-提交验收 5-验收通过 6-验收驳回 7-取消工单 8-申请延期 9-延期审核通过 10-延期审核驳回 (见WorkOrderLogOperTypeEnum)")
     private Integer operType;
 
-    /**
-     * 操作详情描述
-     */
     @ApiModelProperty(value = "操作详情描述")
     private String operDesc;
 
-    /**
-     * 操作人ID
-     */
+    @ApiModelProperty(value = "操作附件 URL,多个使用逗号分隔")
+    private String attachUrl;
+
     @ApiModelProperty(value = "操作人ID")
     private Long operUserId;
 
-    /**
-     * 操作人姓名
-     */
     @ApiModelProperty(value = "操作人姓名")
     private String operUserName;
 
-    /**
-     * 操作时间
-     */
     @ApiModelProperty(value = "操作时间")
-    @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
+    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date operTime;
 }

+ 0 - 60
zk-api-service/src/main/java/com/zksy/api/enums/WorkOrderLevelEnum.java

@@ -1,60 +0,0 @@
-package com.zksy.api.enums;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.util.Objects;
-
-/**
- * 工单优先级枚举(work_order.order_level)
- *
- * @author zengy
- */
-@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));
-    }
-}