Explorar o código

feat(warning): 完善预警系统功能

- 在EarlyWarning实体中新增itemId、itemSnapshot、disposalDeadline、stageDeadline字段
- 实现预警事项绑定功能,包括配置项快照保存和关联逻辑
- 添加预警处置原因选择功能,支持多原因关联和快照保存
- 更新预警处理流程接口,增加reasonIds参数支持
- 优化预警事项编码生成规则,统一编码格式并添加并发控制
- 实现预警事项类型和级别验证功能
- 添加预警原因库管理和查询功能
- 优化预警状态流转逻辑,增加阶段截止时间管理
- 扩展用户查询接口返回部门名称信息
- 调整预警事项权限控制和分页参数校验
林仔 hai 3 semanas
pai
achega
3a8c0c5e31
Modificáronse 21 ficheiros con 330 adicións e 142 borrados
  1. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningController.java
  2. 5 15
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningItemController.java
  3. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningReasonController.java
  4. 15 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/EarlyWarning.java
  5. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningDisposal.java
  6. 3 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningItem.java
  7. 2 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningProcessDTO.java
  8. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/WarningItemMapper.java
  9. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/WarningReasonMapper.java
  10. 2 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/EarlyWarningService.java
  11. 53 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningCatalogRules.java
  12. 69 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningItemBindingService.java
  13. 1 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningItemService.java
  14. 41 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningServiceImpl.java
  15. 47 111
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningItemServiceImpl.java
  16. 15 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningReasonServiceImpl.java
  17. 2 1
      pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml
  18. 5 1
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml
  19. 6 0
      zk-api-service/src/main/java/com/zksy/api/domain/EarlyWarning.java
  20. 15 0
      zk-api-service/src/main/java/com/zksy/api/mapper/WarningItemBindingMapper.java
  21. 32 0
      zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

+ 1 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningController.java

@@ -222,7 +222,7 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
             @RequestPart(value = "files", required = false) List<MultipartFile> files) {
         try {
             WarningProcessDTO process = JSON.parseObject(processJson, WarningProcessDTO.class);
-            boolean result = earlyWarningService.submitWarningProcess(process.getWarningId(), process.getProcessContent(), files);
+            boolean result = earlyWarningService.submitWarningProcess(process.getWarningId(), process.getProcessContent(), files, process.getReasonIds());
             return result ? AjaxResult.success("处理反馈已提交") : AjaxResult.error("处理反馈提交失败");
         } catch (Exception e) {
             log.error("提交预警处理反馈失败", e);

+ 5 - 15
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningItemController.java

@@ -24,7 +24,7 @@ public class WarningItemController {
 
     @GetMapping("/list")
     @ApiOperation("事项信息查询-分页查询预警事项列表")
-    @PreAuthorize("@ss.hasPermi('warning:item:list')")
+    @PreAuthorize("@ss.hasAnyPermi('warning:item:list,warning:warning:add,warning:warning:edit,warning:warning:publish')")
     public AjaxResult queryWarningItems(
             @ApiParam("页码") @RequestParam(defaultValue = "1") Integer pageNum,
             @ApiParam("每页数量") @RequestParam(defaultValue = "10") Integer pageSize,
@@ -34,7 +34,7 @@ public class WarningItemController {
             @ApiParam("预警级别") @RequestParam(required = false) String warningLevel,
             @ApiParam("预警专项") @RequestParam(required = false) String warningSpecial,
             @ApiParam("状态") @RequestParam(required = false) String status) {
-        Page<WarningItem> page = new Page<>(pageNum, pageSize);
+        Page<WarningItem> page = new Page<>(Math.max(1, pageNum), Math.max(1, Math.min(100, pageSize)));
         IPage<WarningItem> result = warningItemService.queryWarningItems(page, itemCode, itemName, 
                                                                           warningType, warningLevel, warningSpecial, status);
         return AjaxResult.success(result);
@@ -102,19 +102,9 @@ public class WarningItemController {
     }
 
     @GetMapping("/code/generate")
-    @ApiOperation("事项编码生成-自动生成事项编码")
+    @ApiOperation("预览事项编码,保存时生成最终唯一编码")
     @PreAuthorize("@ss.hasAnyPermi('warning:item:query,warning:item:add,warning:item:edit')")
-    public AjaxResult generateItemCode(
-            @ApiParam("区域(CN-城南 CB-城北 TC-太常)") @RequestParam String region,
-            @ApiParam("预警类型") @RequestParam String warningType,
-            @ApiParam("排水类型(YS-雨水 WS-污水,非排水类型不传)") @RequestParam(required = false) String drainageType) {
-        String itemCode = warningItemService.generateItemCode(region, warningType, drainageType);
-        return AjaxResult.success("事项编码生成成功", itemCode);
-    }
-
-    /** 保留旧测试及历史调用方的两参数方法,不作为 HTTP 接口暴露。 */
-    public AjaxResult generateItemCode(String warningType, String warningLevel) {
-        String itemCode = warningItemService.generateItemCode(warningType, warningLevel);
-        return AjaxResult.success("事项编码生成成功", itemCode);
+    public AjaxResult generateItemCode(@RequestParam String warningType, @RequestParam String warningLevel) {
+        return AjaxResult.success("编码预览", warningItemService.generateItemCode(warningType, warningLevel));
     }
 }

+ 1 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningReasonController.java

@@ -43,7 +43,7 @@ public class WarningReasonController {
 
     @GetMapping("/query")
     @ApiOperation("预警原因库管理-根据类型和级别查询原因列表")
-    @PreAuthorize("@ss.hasAnyPermi('warning:reason:query,warning:item:query')")
+    @PreAuthorize("@ss.hasAnyPermi('warning:reason:query,warning:item:query,warning:warning:add,warning:warning:edit,warning:warning:handle')")
     public AjaxResult getReasonsByTypeAndLevel(
             @ApiParam("预警类型") @RequestParam String warningType,
             @ApiParam("预警级别") @RequestParam String warningLevel) {

+ 15 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/EarlyWarning.java

@@ -43,6 +43,21 @@ public class EarlyWarning implements Serializable {
     @ApiModelProperty("预警专项")
     private String warningSpecial;
 
+    @TableField(value = "item_id", updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
+    private String itemId;
+
+    /** 关联时保存配置副本,发布后的历史不随事项修改而变化。 */
+    @TableField(value = "item_snapshot", updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
+    private String itemSnapshot;
+
+    @TableField(value = "disposal_deadline", updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime disposalDeadline;
+
+    @TableField(value = "stage_deadline", updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime stageDeadline;
+
 
     @TableField("device_code")
     @ApiModelProperty("关联设备编码")

+ 6 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningDisposal.java

@@ -45,6 +45,12 @@ public class WarningDisposal implements Serializable {
     @ApiModelProperty("处置内容/意见")
     private String disposalContent;
 
+    @TableField("reason_ids")
+    private String reasonIds;
+
+    @TableField("reason_snapshot")
+    private String reasonSnapshot;
+
     @TableField("supervision_user")
     @ApiModelProperty("督办人员")
     private String supervisionUser;

+ 3 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningItem.java

@@ -39,15 +39,9 @@ public class WarningItem implements Serializable {
     @ApiModelProperty("预警专项")
     private String warningSpecial;
 
-    /** 编码生成区域(仅用于生成请求,不落库) */
-    @TableField(exist = false)
-    @ApiModelProperty("编码生成区域")
-    private String region;
-
-    /** 排水编码雨污类型(仅用于生成请求,不落库) */
-    @TableField(exist = false)
-    @ApiModelProperty("排水雨污类型")
-    private String drainageType;
+    @TableField("source_warning_code")
+    @ApiModelProperty("可选采集报警编码,用于匹配系统预警,如 WARN-PRESSURE")
+    private String sourceWarningCode;
 
     @TableField("industry")
     @ApiModelProperty("所属行业")

+ 2 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningProcessDTO.java

@@ -10,4 +10,6 @@ public class WarningProcessDTO {
     private String warningId;
     @NotBlank
     private String processContent;
+
+    private java.util.List<String> reasonIds;
 }

+ 6 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/WarningItemMapper.java

@@ -6,4 +6,10 @@ import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
 public interface WarningItemMapper extends BaseMapper<WarningItem> {
+    @org.apache.ibatis.annotations.Select("SELECT 1 FROM pg_advisory_xact_lock(hashtext('warning-item-code-generation'))")
+    @org.apache.ibatis.annotations.Options(useCache = false, flushCache = org.apache.ibatis.annotations.Options.FlushCachePolicy.TRUE, timeout = 5)
+    Integer lockForCodeGeneration();
+
+    @org.apache.ibatis.annotations.Select("SELECT * FROM app_user.warning_item WHERE item_id = #{itemId} FOR UPDATE")
+    WarningItem selectForUpdate(@org.apache.ibatis.annotations.Param("itemId") String itemId);
 }

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/WarningReasonMapper.java

@@ -6,4 +6,7 @@ import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
 public interface WarningReasonMapper extends BaseMapper<WarningReason> {
+    @org.apache.ibatis.annotations.Select("SELECT 1 FROM pg_advisory_xact_lock(hashtext('warning-reason-code-generation'))")
+    @org.apache.ibatis.annotations.Options(useCache = false, flushCache = org.apache.ibatis.annotations.Options.FlushCachePolicy.TRUE, timeout = 5)
+    Integer lockForCodeGeneration();
 }

+ 2 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/EarlyWarningService.java

@@ -58,6 +58,8 @@ public interface EarlyWarningService extends IService<EarlyWarning> {
 
     boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files);
 
+    boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files, List<String> reasonIds);
+
     boolean clearWarning(String warningId, String clearRemark);
 
     boolean upgradeWarning(String warningId, String newLevel, String disposalContent);

+ 53 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningCatalogRules.java

@@ -0,0 +1,53 @@
+package com.zksy.base.warning.service;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.system.mapper.SysDictDataMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+
+/** 清单、原因库与发布使用同一字典和固定业务环节。 */
+@Component
+public class WarningCatalogRules {
+    @Autowired private SysDictDataMapper dictMapper;
+
+    public void validateTypeAndLevel(String type, String level) {
+        if (dictMapper.selectDictDataByType("alert_type").stream().noneMatch(d -> d.getDictValue().equals(type))) {
+            throw new ServiceException("请选择预警发布模块中启用的预警类型");
+        }
+        if (dictMapper.selectDictDataByType("alert_level").stream().noneMatch(d -> d.getDictValue().equals(level))) {
+            throw new ServiceException("请选择预警发布模块中启用的预警级别");
+        }
+    }
+
+    public static String validateLinks(String value) {
+        JSONArray links;
+        try { links = JSON.parseArray(value); }
+        catch (Exception e) { throw new ServiceException("环节配置格式不正确"); }
+        if (links == null || links.size() != 3) throw new ServiceException("请配置接收、处置、核查三个环节");
+        String[] codes = {"RECEIVE", "HANDLE", "REVIEW"};
+        String[] names = {"接收预警", "现场处置", "核查解除"};
+        for (int i = 0; i < codes.length; i++) {
+            JSONObject link = links.getJSONObject(i);
+            if (link == null || !codes[i].equals(link.getString("code"))) {
+                throw new ServiceException("环节顺序必须为接收、处置、核查");
+            }
+            Integer minutes;
+            try { minutes = link.getInteger("timeLimit"); }
+            catch (Exception e) { throw new ServiceException("环节时限必须是正整数分钟"); }
+            if (minutes == null || minutes < 1 || minutes > 43200
+                    || !String.valueOf(minutes).equals(link.getString("timeLimit"))) {
+                throw new ServiceException("环节时限必须为 1 至 43200 的整数分钟");
+            }
+            if (link.getString("description") != null && link.getString("description").length() > 2000) {
+                throw new ServiceException("环节要求不能超过 2000 字");
+            }
+            link.put("name", names[i]);
+            link.put("handler", i == 2 ? "发布时指定的核查人" : "发布时指定的处置人");
+        }
+        return links.toJSONString();
+    }
+}

+ 69 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningItemBindingService.java

@@ -0,0 +1,69 @@
+package com.zksy.base.warning.service;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.zksy.base.warning.domain.EarlyWarning;
+import com.zksy.base.warning.domain.WarningItem;
+import com.zksy.base.warning.mapper.WarningItemMapper;
+import com.zksy.common.exception.ServiceException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+import java.time.LocalDateTime;
+
+@Service
+public class WarningItemBindingService {
+    @Autowired private WarningItemMapper itemMapper;
+
+    /** 草稿和发布时取最新配置;已发布流程不再读取可变的清单配置。 */
+    public void bind(EarlyWarning warning) {
+        if (warning.getItemId() == null || warning.getItemId().trim().isEmpty()) {
+            warning.setItemId(null);
+            warning.setItemSnapshot(null);
+            return;
+        }
+        WarningItem item = itemMapper.selectForUpdate(warning.getItemId());
+        if (item == null || !"ENABLED".equals(item.getStatus())) {
+            throw new ServiceException("关联事项已禁用或作废,请重新选择启用事项,或取消关联后发布");
+        }
+        if (!Objects.equals(item.getWarningType(), warning.getWarningType())
+                || !Objects.equals(item.getWarningLevel(), warning.getWarningLevel())) {
+            throw new ServiceException("关联事项的预警类型、级别与当前预警不一致,请重新选择");
+        }
+        WarningCatalogRules.validateLinks(item.getLinkConfig());
+        JSONObject snapshot = new JSONObject();
+        snapshot.put("itemId", item.getItemId());
+        snapshot.put("itemCode", item.getItemCode());
+        snapshot.put("itemName", item.getItemName());
+        snapshot.put("warningType", item.getWarningType());
+        snapshot.put("warningLevel", item.getWarningLevel());
+        snapshot.put("warningSpecial", item.getWarningSpecial());
+        snapshot.put("industry", item.getIndustry());
+        snapshot.put("duration", item.getDuration());
+        snapshot.put("linkConfig", JSON.parseArray(item.getLinkConfig()));
+        warning.setItemSnapshot(snapshot.toJSONString());
+    }
+
+    public void startPublished(EarlyWarning warning, LocalDateTime now) {
+        warning.setDisposalDeadline(null);
+        if (warning.getItemSnapshot() != null) {
+            Integer duration = JSON.parseObject(warning.getItemSnapshot()).getInteger("duration");
+            if (duration != null && duration > 0) warning.setDisposalDeadline(now.plusHours(duration));
+        }
+        startStage(warning, "RECEIVE", now);
+    }
+
+    public void startStage(EarlyWarning warning, String code, LocalDateTime now) {
+        warning.setStageDeadline(null);
+        if (warning.getItemSnapshot() == null) return;
+        JSONObject snapshot = JSON.parseObject(warning.getItemSnapshot());
+        for (Object value : snapshot.getJSONArray("linkConfig")) {
+            JSONObject link = (JSONObject) value;
+            if (code.equals(link.getString("code"))) {
+                warning.setStageDeadline(now.plusMinutes(link.getIntValue("timeLimit")));
+                break;
+            }
+        }
+    }
+}

+ 1 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningItemService.java

@@ -23,8 +23,6 @@ public interface WarningItemService extends IService<WarningItem> {
 
     WarningItem getItemByCode(String itemCode);
 
-    String generateItemCode(String region, String warningType, String drainageType);
-
-    /** 兼容旧客户端的编码生成规则。 */
+    /** 按预警类型和等级生成编码预览,保存时在事务锁内重新取号。 */
     String generateItemCode(String warningType, String warningLevel);
 }

+ 41 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningServiceImpl.java

@@ -37,6 +37,8 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
 
     @Autowired
     private EarlyWarningMapper earlyWarningMapper;
+    @Autowired private com.zksy.base.warning.service.WarningItemBindingService itemBinding;
+    @Autowired private WarningReasonMapper warningReasonMapper;
     @Autowired
     private AlarmWarningRelMapper alarmWarningRelMapper;
     @Autowired
@@ -200,6 +202,9 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         warning.setCreateTime(LocalDateTime.now());
         warning.setUpdateTime(LocalDateTime.now());
 
+        itemBinding.bind(warning);
+        warning.setDisposalDeadline(null);
+        warning.setStageDeadline(null);
         boolean result = save(warning);
         if (result) {
             if (alarmIds != null && !alarmIds.isEmpty()) {
@@ -266,6 +271,9 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     public boolean updateWarning(EarlyWarning warning, List<String> alarmIds, List<MultipartFile> files) {
         EarlyWarning existing = earlyWarningMapper.selectForUpdate(warning.getWarningId());
         WarningWorkflow.requireState(existing, "修改草稿", WarningWorkflow.DRAFT);
+        itemBinding.bind(warning);
+        warning.setDisposalDeadline(null);
+        warning.setStageDeadline(null);
         warning.setStatus(existing.getStatus());
         warning.setWarningNo(existing.getWarningNo());
         warning.setCreateBy(existing.getCreateBy());
@@ -369,10 +377,12 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         requireActiveUser(reviewer, "核查人");
         warning.setReviewer(reviewer);
 
+        itemBinding.bind(warning);
         warning.setStatus("PENDING");
         warning.setPublisher(currentUser());
         warning.setUpdateBy(currentUser());
         warning.setPublishTime(LocalDateTime.now());
+        itemBinding.startPublished(warning, warning.getPublishTime());
         warning.setUpdateTime(LocalDateTime.now());
         warning.setHandler(assignee);
 
@@ -401,6 +411,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         String assignee = warning.getHandler();
         // 接收预警:待办(PENDING) → 已接收(RECEIVED)
         warning.setStatus("RECEIVED");
+        itemBinding.startStage(warning, "HANDLE", now);
         warning.setHandler(assignee);
         warning.setUpdateTime(now);
         if (!updateById(warning)) return false;
@@ -434,6 +445,12 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files) {
+        return submitWarningProcess(warningId, processContent, files, Collections.emptyList());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files, List<String> reasonIds) {
         EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
         WarningWorkflow.requireState(warning, "提交处置结果", WarningWorkflow.PROCESSING);
         requireHandler(warning);
@@ -441,6 +458,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         LocalDateTime now = LocalDateTime.now();
         String operator = currentUser();
         warning.setStatus("HANDLED");
+        itemBinding.startStage(warning, "REVIEW", now);
         warning.setRemark(processContent);
         warning.setUpdateTime(now);
         if (!updateById(warning)) return false;
@@ -449,6 +467,27 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         disposal.setDisposalType("HANDLE");
         disposal.setDisposalUser(operator);
         disposal.setDisposalContent(processContent);
+        List<String> selectedIds = reasonIds == null ? Collections.emptyList() : new ArrayList<>(new LinkedHashSet<>(reasonIds));
+        if (selectedIds.size() > 20) throw new ServiceException("一次最多选择 20 个处置原因");
+        List<Map<String, Object>> reasonSnapshot = new ArrayList<>();
+        for (String reasonId : selectedIds) {
+            WarningReason reason = warningReasonMapper.selectById(reasonId);
+            if (reason == null || !"0".equals(reason.getStatus())
+                    || !Objects.equals(reason.getWarningType(), warning.getWarningType())
+                    || !Objects.equals(reason.getWarningLevel(), warning.getWarningLevel())) {
+                throw new ServiceException("所选原因已停用或不属于当前预警类型和级别,请重新选择");
+            }
+            Map<String, Object> entry = new LinkedHashMap<>();
+            entry.put("reasonId", reason.getReasonId());
+            entry.put("reasonCode", reason.getReasonCode());
+            entry.put("reasonName", reason.getReasonName());
+            entry.put("description", reason.getDescription());
+            entry.put("warningType", reason.getWarningType());
+            entry.put("warningLevel", reason.getWarningLevel());
+            reasonSnapshot.add(entry);
+        }
+        disposal.setReasonIds(com.alibaba.fastjson2.JSON.toJSONString(selectedIds));
+        disposal.setReasonSnapshot(com.alibaba.fastjson2.JSON.toJSONString(reasonSnapshot));
         disposal.setCreateTime(now);
         warningDisposalMapper.insert(disposal);
         if (files != null && !files.isEmpty()) {
@@ -469,6 +508,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         LocalDateTime now = LocalDateTime.now();
         String operator = currentUser();
         warning.setStatus("CLOSED");
+        warning.setStageDeadline(null);
         warning.setRemark(clearRemark);
         warning.setUpdateTime(now);
         if (!updateById(warning)) return false;
@@ -585,6 +625,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         if (isBlank(disposalContent)) throw new ServiceException("请填写退回原因和重新处置要求");
         requireActiveUser(warning.getHandler(), "原处置人");
         warning.setStatus("PROCESSING");
+        itemBinding.startStage(warning, "HANDLE", LocalDateTime.now());
         warning.setUpdateBy(currentUser());
         warning.setUpdateTime(LocalDateTime.now());
 

+ 47 - 111
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningItemServiceImpl.java

@@ -8,6 +8,11 @@ import com.zksy.base.warning.domain.WarningItem;
 import com.zksy.base.warning.mapper.WarningItemMapper;
 import com.zksy.base.warning.service.WarningItemService;
 import lombok.extern.slf4j.Slf4j;
+import com.zksy.base.warning.service.WarningCatalogRules;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.Objects;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -17,14 +22,14 @@ import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
-import java.util.Collections;
 
 @Slf4j
 @Service
 public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, WarningItem> implements WarningItemService {
 
+    @Autowired private WarningCatalogRules catalogRules;
+
     private static final Set<String> ALLOWED_STATUSES =
             new HashSet<>(Arrays.asList("ENABLED", "DISABLED", "INVALID"));
 
@@ -60,17 +65,11 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
     @Transactional(rollbackFor = Exception.class)
     public boolean addWarningItem(WarningItem warningItem) {
         validateWarningItem(warningItem, false);
-        if (warningItem.getItemCode() == null || warningItem.getItemCode().isEmpty()) {
-            if (warningItem.getRegion() != null && !warningItem.getRegion().trim().isEmpty()) {
-                warningItem.setItemCode(generateItemCode(warningItem.getRegion(), warningItem.getWarningType(), warningItem.getDrainageType()));
-            } else {
-                // Existing integrations may still submit only warningType/warningLevel.
-                warningItem.setItemCode(generateItemCode(warningItem.getWarningType(), warningItem.getWarningLevel()));
-            }
-        }
-        if (getItemByCode(warningItem.getItemCode()) != null) {
-            throw new IllegalArgumentException("事项编码已存在:" + warningItem.getItemCode());
-        }
+        baseMapper.lockForCodeGeneration();
+        warningItem.setItemId(null);
+        warningItem.setItemCode(generateItemCode(warningItem.getWarningType(), warningItem.getWarningLevel()));
+        warningItem.setCreateBy(SecurityUtils.getUsername());
+        warningItem.setUpdateBy(SecurityUtils.getUsername());
         if (warningItem.getStatus() == null) {
             warningItem.setStatus("ENABLED");
         }
@@ -82,10 +81,17 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
     @Transactional(rollbackFor = Exception.class)
     public boolean updateWarningItem(WarningItem warningItem) {
         validateWarningItem(warningItem, true);
-        WarningItem sameCodeItem = getItemByCode(warningItem.getItemCode());
-        if (sameCodeItem != null && !sameCodeItem.getItemId().equals(warningItem.getItemId())) {
-            throw new IllegalArgumentException("事项编码已存在:" + warningItem.getItemCode());
-        }
+        WarningItem existing = baseMapper.selectForUpdate(warningItem.getItemId());
+        requireEditable(existing);
+        if (!Objects.equals(existing.getWarningType(), warningItem.getWarningType())
+                || !Objects.equals(existing.getWarningLevel(), warningItem.getWarningLevel())) {
+            throw new ServiceException("事项类型和级别确定后不可修改,请新增事项配置");
+        }
+        warningItem.setItemCode(existing.getItemCode());
+        warningItem.setStatus(existing.getStatus());
+        warningItem.setCreateBy(existing.getCreateBy());
+        warningItem.setCreateTime(existing.getCreateTime());
+        warningItem.setUpdateBy(SecurityUtils.getUsername());
         warningItem.setUpdateTime(LocalDateTime.now());
         return this.updateById(warningItem);
     }
@@ -99,7 +105,11 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
         if (!ALLOWED_STATUSES.contains(status)) {
             throw new IllegalArgumentException("不支持的事项状态:" + status);
         }
+        WarningItem existing = baseMapper.selectForUpdate(itemId);
+        requireEditable(existing);
+        if ("ENABLED".equals(status)) validateWarningItem(existing, true);
         WarningItem item = new WarningItem();
+        item.setUpdateBy(SecurityUtils.getUsername());
         item.setItemId(itemId);
         item.setStatus(status);
         item.setUpdateTime(LocalDateTime.now());
@@ -118,43 +128,12 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
         return this.getOne(wrapper);
     }
 
-    @Override
-    public String generateItemCode(String region, String warningType, String drainageType) {
-        String regionCode = normalizeCode(region, REGION_CODES, "区域", false);
-        String typeCode = normalizeCode(warningType, TYPE_CODES, "预警类型", false);
-        String subtypeCode = "";
-        if ("PS".equals(typeCode)) {
-            subtypeCode = normalizeCode(drainageType, DRAINAGE_CODES, "排水类型", true);
-        }
-
-        String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
-        String prefix = regionCode + typeCode + subtypeCode;
-        String codePrefix = prefix + dateStr;
-        LambdaQueryWrapper<WarningItem> wrapper = new LambdaQueryWrapper<>();
-        wrapper.likeRight(WarningItem::getItemCode, codePrefix);
-        wrapper.orderByDesc(WarningItem::getItemCode);
-        wrapper.last("LIMIT 1");
-
-        WarningItem lastItem = this.getOne(wrapper);
-        int sequence = nextSequence(lastItem, codePrefix);
-        return codePrefix + String.format("%04d", sequence);
-    }
-
-    /**
-     * Legacy format retained for existing callers that have not migrated to region/type parameters.
-     */
     @Override
     public String generateItemCode(String warningType, String warningLevel) {
-        if (warningType == null || warningType.trim().isEmpty()) {
-            throw new IllegalArgumentException("预警类型不能为空");
-        }
-        if (warningLevel == null || warningLevel.trim().isEmpty()) {
-            throw new IllegalArgumentException("预警级别不能为空");
-        }
-        warningType = warningType.trim();
-        warningLevel = warningLevel.trim();
+        catalogRules.validateTypeAndLevel(warningType, warningLevel);
+        if (!warningType.matches("[A-Za-z0-9_]{1,32}")) throw new ServiceException("预警类型编码仅支持字母、数字和下划线");
         String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
-        String prefix = "WI" + warningType.substring(0, Math.min(3, warningType.length())).toUpperCase() + warningLevel;
+        String prefix = "WI-" + warningType.toUpperCase(Locale.ROOT) + "-L" + warningLevel + "-";
         
         LambdaQueryWrapper<WarningItem> wrapper = new LambdaQueryWrapper<>();
         wrapper.likeRight(WarningItem::getItemCode, prefix + dateStr);
@@ -162,21 +141,12 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
         wrapper.last("LIMIT 1");
         
         WarningItem lastItem = this.getOne(wrapper);
-        int sequence = 1;
-        
-        if (lastItem != null) {
-            String lastCode = lastItem.getItemCode();
-            if (lastCode != null && lastCode.startsWith(prefix + dateStr) && lastCode.length() >= 4) {
-                String lastSeqStr = lastCode.substring(lastCode.length() - 4);
-                try {
-                    sequence = Integer.parseInt(lastSeqStr) + 1;
-                } catch (NumberFormatException ignored) {
-                    sequence = 1;
-                }
-            }
-        }
-        
-        return prefix + dateStr + String.format("%04d", sequence);
+        return prefix + dateStr + String.format("%04d", nextSequence(lastItem, prefix + dateStr));
+    }
+
+    private void requireEditable(WarningItem item) {
+        if (item == null) throw new ServiceException("预警事项不存在");
+        if ("INVALID".equals(item.getStatus())) throw new ServiceException("作废事项不可修改或重新启用");
     }
 
     private int nextSequence(WarningItem lastItem, String codePrefix) {
@@ -187,7 +157,7 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
         try {
             int sequence = Integer.parseInt(lastSeqStr) + 1;
             if (sequence > 9999) {
-                throw new IllegalStateException("当天该区域、类型和排水分类的事项编码已达到流水号上限");
+                throw new IllegalStateException("当天该类型和级别的事项编码已达到流水号上限");
             }
             return sequence;
         } catch (NumberFormatException ex) {
@@ -195,48 +165,6 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
         }
     }
 
-    private static final Map<String, String> REGION_CODES;
-    private static final Map<String, String> TYPE_CODES;
-    private static final Map<String, String> DRAINAGE_CODES;
-
-    static {
-        Map<String, String> regions = new java.util.HashMap<>();
-        regions.put("CN", "CN"); regions.put("城南", "CN");
-        regions.put("CB", "CB"); regions.put("城北", "CB");
-        regions.put("TC", "TC"); regions.put("太常", "TC");
-        REGION_CODES = Collections.unmodifiableMap(regions);
-
-        Map<String, String> types = new java.util.HashMap<>();
-        types.put("RQ", "RQ"); types.put("燃气", "RQ"); types.put("GAS_PIPE", "RQ");
-        types.put("GS", "GS"); types.put("供水", "GS"); types.put("WATER_PIPE", "GS");
-        types.put("PS", "PS"); types.put("排水", "PS"); types.put("SEWER_PIPE", "PS");
-        types.put("YJ", "YJ"); types.put("窨井", "YJ"); types.put("MANHOLE", "YJ");
-        TYPE_CODES = Collections.unmodifiableMap(types);
-
-        Map<String, String> drainage = new java.util.HashMap<>();
-        drainage.put("YS", "YS"); drainage.put("雨水", "YS"); drainage.put("RAIN", "YS");
-        drainage.put("WS", "WS"); drainage.put("污水", "WS"); drainage.put("SEWAGE", "WS");
-        DRAINAGE_CODES = Collections.unmodifiableMap(drainage);
-    }
-
-    private String normalizeCode(String value, Map<String, String> aliases, String fieldName, boolean required) {
-        if (value == null || value.trim().isEmpty()) {
-            if (required) {
-                throw new IllegalArgumentException(fieldName + "不能为空");
-            }
-            throw new IllegalArgumentException(fieldName + "不能为空");
-        }
-        String normalized = value.trim().toUpperCase(Locale.ROOT);
-        String code = aliases.get(normalized);
-        if (code == null) {
-            code = aliases.get(value.trim());
-        }
-        if (code == null) {
-            throw new IllegalArgumentException("不支持的" + fieldName + ":" + value);
-        }
-        return code;
-    }
-
     private void validateWarningItem(WarningItem warningItem, boolean update) {
         if (warningItem == null) {
             throw new IllegalArgumentException("预警事项不能为空");
@@ -253,8 +181,16 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
         if (warningItem.getWarningLevel() == null || warningItem.getWarningLevel().trim().isEmpty()) {
             throw new IllegalArgumentException("预警级别不能为空");
         }
-        if (warningItem.getDuration() != null && warningItem.getDuration() <= 0) {
-            throw new IllegalArgumentException("预警时长必须大于0");
+        if (warningItem.getDuration() == null || warningItem.getDuration() <= 0 || warningItem.getDuration() > 720) {
+            throw new IllegalArgumentException("处置时限必须为 1 至 720 小时");
+        }
+        catalogRules.validateTypeAndLevel(warningItem.getWarningType(), warningItem.getWarningLevel());
+        warningItem.setLinkConfig(WarningCatalogRules.validateLinks(warningItem.getLinkConfig()));
+        if (warningItem.getIndustry() == null || warningItem.getIndustry().trim().isEmpty()) throw new ServiceException("请选择所属行业");
+        String source = warningItem.getSourceWarningCode();
+        warningItem.setSourceWarningCode(source == null ? "" : source.trim().toUpperCase(Locale.ROOT));
+        if (!warningItem.getSourceWarningCode().isEmpty() && !warningItem.getSourceWarningCode().matches("[A-Z0-9_-]{1,64}")) {
+            throw new ServiceException("采集报警编码仅支持字母、数字、横线和下划线,最多 64 位");
         }
         if (warningItem.getStatus() != null && !ALLOWED_STATUSES.contains(warningItem.getStatus())) {
             throw new IllegalArgumentException("不支持的事项状态:" + warningItem.getStatus());

+ 15 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningReasonServiceImpl.java

@@ -23,6 +23,9 @@ import java.util.Set;
 @Service
 public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, WarningReason> implements WarningReasonService {
 
+    @org.springframework.beans.factory.annotation.Autowired
+    private com.zksy.base.warning.service.WarningCatalogRules catalogRules;
+
     private static final Set<String> ALLOWED_STATUSES = new HashSet<>(Arrays.asList("0", "1"));
 
     @Override
@@ -64,6 +67,10 @@ public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, W
     @Transactional(rollbackFor = Exception.class)
     public boolean addWarningReason(WarningReason warningReason) {
         validateWarningReason(warningReason, false);
+        baseMapper.lockForCodeGeneration();
+        warningReason.setReasonId(null);
+        warningReason.setReasonCode(null);
+        warningReason.setCreateBy(com.zksy.common.utils.SecurityUtils.getUsername());
         if (warningReason.getReasonCode() == null || warningReason.getReasonCode().isEmpty()) {
             warningReason.setReasonCode(generateReasonCode(warningReason.getWarningType(), warningReason.getWarningLevel()));
         }
@@ -84,6 +91,12 @@ public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, W
     @Transactional(rollbackFor = Exception.class)
     public boolean updateWarningReason(WarningReason warningReason) {
         validateWarningReason(warningReason, true);
+        WarningReason existing = getById(warningReason.getReasonId());
+        if (existing == null) throw new com.zksy.common.exception.ServiceException("预警原因不存在");
+        warningReason.setReasonCode(existing.getReasonCode());
+        warningReason.setCreateBy(existing.getCreateBy());
+        warningReason.setCreateTime(existing.getCreateTime());
+        warningReason.setUpdateBy(com.zksy.common.utils.SecurityUtils.getUsername());
         WarningReason sameCodeReason = getReasonByCode(warningReason.getReasonCode());
         if (sameCodeReason != null && !sameCodeReason.getReasonId().equals(warningReason.getReasonId())) {
             throw new IllegalArgumentException("原因编码已存在:" + warningReason.getReasonCode());
@@ -102,6 +115,7 @@ public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, W
             throw new IllegalArgumentException("不支持的原因状态:" + status);
         }
         WarningReason reason = new WarningReason();
+        reason.setUpdateBy(com.zksy.common.utils.SecurityUtils.getUsername());
         reason.setReasonId(reasonId);
         reason.setStatus(status);
         reason.setUpdateTime(LocalDateTime.now());
@@ -176,6 +190,7 @@ public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, W
         if (warningReason.getSortOrder() != null && warningReason.getSortOrder() < 0) {
             throw new IllegalArgumentException("排序值不能小于0");
         }
+        catalogRules.validateTypeAndLevel(warningReason.getWarningType(), warningReason.getWarningLevel());
         if (warningReason.getStatus() != null && !ALLOWED_STATUSES.contains(warningReason.getStatus())) {
             throw new IllegalArgumentException("不支持的原因状态:" + warningReason.getStatus());
         }

+ 2 - 1
pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml

@@ -90,8 +90,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	</select>
 	
     <select id="selectAllEnabledUser" resultMap="SysUserResult">
-        select u.user_id, u.dept_id, u.nick_name, u.user_name
+        select u.user_id, u.dept_id, u.nick_name, u.user_name, d.dept_name
         from app_user.sys_user u
+        left join app_user.sys_dept d on d.dept_id = u.dept_id
         where u.del_flag = '0' and u.status = '0'
         order by u.nick_name
     </select>

+ 5 - 1
pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml

@@ -9,6 +9,10 @@
         <result column="warning_type" property="warningType"/>
         <result column="warning_level" property="warningLevel"/>
         <result column="warning_special" property="warningSpecial"/>
+        <result column="disposal_deadline" property="disposalDeadline"/>
+        <result column="stage_deadline" property="stageDeadline"/>
+        <result column="item_id" property="itemId"/>
+        <result column="item_snapshot" property="itemSnapshot"/>
         <result column="device_code" property="deviceCode"/>
         <result column="device_name" property="deviceName"/>
         <result column="location" property="location"/>
@@ -31,7 +35,7 @@
 
     <sql id="Base_Column_List">
         warning_id, warning_no, warning_name, warning_type, warning_level, warning_special,
-        device_code, device_name,
+        disposal_deadline, stage_deadline, item_id, item_snapshot, device_code, device_name,
         location, longitude, latitude, ownership_unit, publisher, publish_time, handler, reviewer,
         status, warning_content, process_instance_id, create_by, create_time, update_by,
         update_time, remark

+ 6 - 0
zk-api-service/src/main/java/com/zksy/api/domain/EarlyWarning.java

@@ -43,6 +43,12 @@ public class EarlyWarning implements Serializable {
     @ApiModelProperty("预警专项")
     private String warningSpecial;
 
+    @TableField(value = "item_id", updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
+    private String itemId;
+
+    @TableField(value = "item_snapshot", updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
+    private String itemSnapshot;
+
 
     @TableField("device_code")
     @ApiModelProperty("关联设备编码")

+ 15 - 0
zk-api-service/src/main/java/com/zksy/api/mapper/WarningItemBindingMapper.java

@@ -0,0 +1,15 @@
+package com.zksy.api.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface WarningItemBindingMapper {
+    @Select("SELECT item_id, item_code, item_name, warning_type, warning_level, warning_special, industry, duration, link_config "
+            + "FROM app_user.warning_item WHERE source_warning_code = #{code} AND warning_level = #{level} "
+            + "AND status = 'ENABLED' ORDER BY item_id LIMIT 2")
+    List<Map<String, Object>> findEnabled(@Param("code") String code, @Param("level") String level);
+}

+ 32 - 0
zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

@@ -39,6 +39,9 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
     @Autowired
     private EquipmentBaseApiMapper equipmentBaseApiMapper;
 
+    @Autowired
+    private com.zksy.api.mapper.WarningItemBindingMapper warningItemBindingMapper;
+
     @Override
     public Integer checkAndGenerate(String deviceCode, String warningType, String warningCode,
                                     BigDecimal minValue, BigDecimal maxValue, BigDecimal actualValue,
@@ -124,6 +127,7 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
                 }
                 existing.setUpdateBy(SYSTEM_USER);
                 existing.setUpdateTime(LocalDateTime.now());
+                bindConfiguredItem(existing, warningCode);
                 requireSaved(earlyWarningApiMapper.updateById(existing) > 0);
                 log.info("更新主动预警: warningNo={}, deviceCode={}, warningCode={}, level={}级",
                         existing.getWarningNo(), deviceCode, warningCode, level);
@@ -164,6 +168,7 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
         }
         existing.setUpdateBy(SYSTEM_USER);
         existing.setUpdateTime(LocalDateTime.now());
+        bindConfiguredItem(existing, existing.getWarningSpecial());
         requireSaved(earlyWarningApiMapper.updateById(existing) > 0);
         if (levelChanged) {
             log.info("自动更新预警级别: warningNo={}, level={}级, approachRatio={}%",
@@ -210,12 +215,39 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
             warning.setLocation("设备编码:" + deviceCode);
         }
 
+        bindConfiguredItem(warning, warningCode);
         requireSaved(earlyWarningApiMapper.insert(warning) > 0);
         log.info("自动生成预警成功: warningNo={}, deviceCode={}, warningCode={}, level={}级, actual={}, min={}, max={}",
                 warning.getWarningNo(), deviceCode, warningCode, result.getLevel(),
                 actualValue, minValue, maxValue);
     }
 
+    /** 匹配不明确或规则读取失败时仍保存原始草稿,不能影响采集。 */
+    private void bindConfiguredItem(EarlyWarning warning, String warningCode) {
+        warning.setItemId(null);
+        warning.setItemSnapshot(null);
+        try {
+            List<java.util.Map<String, Object>> items = warningItemBindingMapper.findEnabled(warningCode, warning.getWarningLevel());
+            if (items.size() != 1) return;
+            java.util.Map<String, Object> item = items.get(0);
+            com.alibaba.fastjson.JSONObject snapshot = new com.alibaba.fastjson.JSONObject();
+            snapshot.put("itemId", item.get("item_id"));
+            snapshot.put("itemCode", item.get("item_code"));
+            snapshot.put("itemName", item.get("item_name"));
+            snapshot.put("warningType", item.get("warning_type"));
+            snapshot.put("warningLevel", item.get("warning_level"));
+            snapshot.put("warningSpecial", item.get("warning_special"));
+            snapshot.put("industry", item.get("industry"));
+            snapshot.put("duration", item.get("duration"));
+            snapshot.put("linkConfig", com.alibaba.fastjson.JSON.parseArray(java.util.Objects.toString(item.get("link_config"), "[]")));
+            warning.setItemId(java.util.Objects.toString(item.get("item_id"), null));
+            warning.setItemSnapshot(snapshot.toJSONString());
+            warning.setWarningType(java.util.Objects.toString(item.get("warning_type"), warning.getWarningType()));
+        } catch (Exception e) {
+            log.warn("预警清单匹配失败,保留未关联草稿: warningCode={}", warningCode, e);
+        }
+    }
+
     private EquipmentBase findEquipment(String deviceCode) {
         try {
             return equipmentBaseApiMapper.selectOne(new LambdaQueryWrapper<EquipmentBase>()