Sfoglia il codice sorgente

feat(warning): 优化预警管理系统状态流程和功能

- 添加 RECEIVED 状态支持预警接收流程
- 修改 AlarmWarningRelMapper.xml 添加 alarm_no 字段
- 更新 DrainageWarning 和 EarlyWarning 实体类状态定义
- 优化预警控制器参数验证和状态筛选逻辑
- 添加预警报告上传功能和督办附件处理
- 实现预警接收和开始处置的工作流操作
- 增加预警归档和通知读取功能
- 优化数据库查询支持新的状态流转
- 添加事务回滚时的文件清理机制
- 完善预警处理过程中的状态校验和权限控制
林仔 3 settimane fa
parent
commit
6e4924fab7
21 ha cambiato i file con 528 aggiunte e 126 eliminazioni
  1. 3 3
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageWarningController.java
  2. 41 16
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningController.java
  3. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningTodayController.java
  4. 1 0
      pipe-network-service/zksy-common/src/main/java/com/zksy/common/sms/SmsProperties.java
  5. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/EarlyWarning.java
  6. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningAttachment.java
  7. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningSupervision.java
  8. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningTodo.java
  9. 50 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningWorkflow.java
  10. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/EarlyWarningMapper.java
  11. 11 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/EarlyWarningService.java
  12. 45 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningSupervisionNotifier.java
  13. 289 85
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningServiceImpl.java
  14. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/BusinessCockpitEventAdapter.java
  15. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/DrainageWarning.java
  16. 3 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageWarningService.java
  17. 2 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageWarningServiceImpl.java
  18. 1 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/AlarmWarningRelMapper.xml
  19. 46 7
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml
  20. 4 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/WarningDisposalMapper.xml
  21. 5 6
      zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

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

@@ -39,7 +39,7 @@ public class DrainageWarningController {
             @ApiParam("每页数量") @RequestParam(defaultValue = "10") long pageSize,
             @ApiParam("预警级别(1严重,2重要,3一般)") @RequestParam(required = false) Integer warningLevel,
             @ApiParam("预警类型") @RequestParam(required = false) String warningType,
-            @ApiParam("状态(DRAFT/PENDING/PROCESSING/RELEASED/HANDLED/CLOSED)") @RequestParam(required = false) String status,
+            @ApiParam("状态(DRAFT/PENDING/RECEIVED/PROCESSING/HANDLED/CLOSED)") @RequestParam(required = false) String status,
             @ApiParam("开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String startTime,
             @ApiParam("结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String endTime) {
         return service.findByPage(pageNum, pageSize, warningLevel, warningType, status, startTime, endTime);
@@ -56,7 +56,7 @@ public class DrainageWarningController {
             @ApiParam("当前用户ID") @RequestParam(required = false) Long userId,
             @ApiParam("当前用户部门ID") @RequestParam(required = false) Long deptId,
             @ApiParam("是否管理员") @RequestParam(defaultValue = "false") boolean isAdmin,
-            @ApiParam("状态筛选(DRAFT/PENDING/PROCESSING/RELEASED),不传则返回所有当前状态") @RequestParam(required = false) String status,
+            @ApiParam("状态筛选(DRAFT/PENDING/RECEIVED/PROCESSING),不传则返回所有当前状态") @RequestParam(required = false) String status,
             @ApiParam("是否只返回当前预警") @RequestParam(defaultValue = "false") boolean currentOnly,
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String startTime,
@@ -120,7 +120,7 @@ public class DrainageWarningController {
     @Transactional
     public AjaxResult updateStatus(
             @PathVariable String id,
-            @ApiParam("新状态(DRAFT/PENDING/PROCESSING/RELEASED/HANDLED/CLOSED)") @RequestParam String status,
+            @ApiParam("新状态(DRAFT/PENDING/RECEIVED/PROCESSING/HANDLED/CLOSED)") @RequestParam String status,
             @ApiParam("处理人") @RequestParam(required = false) String processor) {
         boolean updated = service.updateStatus(id, status, processor);
         return updated ? AjaxResult.success("状态更新成功") : AjaxResult.error("状态更新失败");

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

@@ -75,11 +75,9 @@ public class EarlyWarningController {
     public AjaxResult getTodoList(
             @ApiParam("页码") @RequestParam(defaultValue = "1") Integer pageNum,
             @ApiParam("每页数量") @RequestParam(defaultValue = "10") Integer pageSize,
-            @ApiParam("用户名") @RequestParam String userName,
-            @ApiParam("待办类型") @RequestParam(required = false) String todoType,
-            @ApiParam("预警名称") @RequestParam(required = false) String warningName) {
-        Page<?> page = new Page<>(pageNum, pageSize);
-        IPage<Map<String, Object>> result = earlyWarningService.getTodoList(page, userName, todoType, warningName);
+            @RequestParam Map<String, String> filters) {
+        Page<?> page = new Page<>(Math.max(1, pageNum), Math.max(1, Math.min(100, pageSize)));
+        IPage<Map<String, Object>> result = earlyWarningService.getTodoList(page, filters);
         return AjaxResult.success(result);
     }
 
@@ -126,9 +124,9 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
             @RequestPart(value = "files", required = false) List<MultipartFile> files) {
         try {
             EarlyWarning warning = JSON.parseObject(warningJson, EarlyWarning.class);
-            List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty() 
-                ? JSON.parseArray(alarmIdsJson, String.class) 
-                : new ArrayList<>();
+            List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
+                ? JSON.parseArray(alarmIdsJson, String.class)
+                : null;
             
             boolean result = earlyWarningService.updateWarning(warning, alarmIds, files);
             return result ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
@@ -161,7 +159,8 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
     @Log(title = "发布预警", businessType = BusinessType.UPDATE)
     @PreAuthorize("@ss.hasPermi('warning:warning:publish')")
     public AjaxResult publishWarning(@PathVariable String warningId,
-                                     @RequestParam(required = false) String handler) {
+                                     @RequestParam(required = false) String handler,
+                                     @RequestParam(required = false) String reviewer) {
         // 允许草稿在编辑时先保存处置人,发布接口仍必须确认最终处置人存在。
         if (handler == null || handler.trim().isEmpty()) {
             EarlyWarning warning = earlyWarningService.getById(warningId);
@@ -173,19 +172,38 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
         if (handler == null || handler.trim().isEmpty()) {
             return AjaxResult.error("发布预警前必须选择处置人");
         }
-        boolean result = earlyWarningService.publishWarning(warningId, handler.trim());
+        boolean result = earlyWarningService.publishWarning(warningId, handler.trim(), reviewer);
         return result ? AjaxResult.success("发布成功") : AjaxResult.error("发布失败");
     }
 
+    @PostMapping("/report/{warningId}")
+    @ApiOperation("上传预警报告,不修改流程状态")
+    @PreAuthorize("@ss.hasPermi('warning:warning:edit')")
+    public AjaxResult uploadReport(@PathVariable String warningId,
+                                   @RequestPart("files") List<MultipartFile> files) {
+        return earlyWarningService.uploadReport(warningId, files)
+                ? AjaxResult.success("报告上传成功") : AjaxResult.error("报告上传失败");
+    }
+
     @PostMapping("/confirm")
-    @ApiOperation("确认异常并分配处理人")
+    @ApiOperation("接收预警")
     @PreAuthorize("@ss.hasPermi('warning:warning:handle')")
     public AjaxResult confirmWarning(
             @RequestParam String warningId,
             @RequestParam(required = false) String assignedUser,
             @RequestParam(required = false) String confirmRemark) {
         boolean result = earlyWarningService.confirmWarning(warningId, assignedUser, confirmRemark);
-        return result ? AjaxResult.success("预警已确认并进入处理") : AjaxResult.error("预警确认失败");
+        return result ? AjaxResult.success("预警已接收") : AjaxResult.error("预警接收失败");
+    }
+
+    @PostMapping("/start-process")
+    @ApiOperation("开始处置预警")
+    @PreAuthorize("@ss.hasPermi('warning:warning:handle')")
+    public AjaxResult startWarningProcess(
+            @RequestParam String warningId,
+            @RequestParam(required = false) String remark) {
+        boolean result = earlyWarningService.startWarningProcess(warningId, remark);
+        return result ? AjaxResult.success("已开始处置") : AjaxResult.error("开始处置失败");
     }
 
     @PostMapping("/misreport")
@@ -264,9 +282,16 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
             @ApiParam("督办意见") @RequestParam String supervisionContent,
             @ApiParam("通知方式") @RequestParam String notificationType,
             @ApiParam("通知内容") @RequestParam(required = false) String notificationContent,
-            @ApiParam("附件地址") @RequestParam(required = false) String attachmentUrl) {
-        boolean result = earlyWarningService.supervisionWarning(warningId, supervisionUser, supervisionContent, notificationType, notificationContent, attachmentUrl);
-        return result ? AjaxResult.success("督办成功") : AjaxResult.error("督办失败");
+            @ApiParam("督办附件") @RequestParam(required = false) MultipartFile attachment) {
+        WarningSupervision result = earlyWarningService.supervisionWarning(warningId, supervisionUser, supervisionContent, notificationType, notificationContent, attachment);
+        return AjaxResult.success("FAILED".equals(result.getNotificationStatus())
+                ? "督办已保存,但短信发送失败" : "督办通知已发送", result);
+    }
+
+    @PostMapping("/notifications/{todoId}/read")
+    @PreAuthorize("@ss.hasPermi('warning:warning:list')")
+    public AjaxResult readNotification(@PathVariable String todoId) {
+        return earlyWarningService.readNotification(todoId) ? AjaxResult.success() : AjaxResult.error("通知不存在或无权访问");
     }
 
     @PostMapping("/instruction")
@@ -479,7 +504,7 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
         if (attachmentName == null || attachmentName.trim().isEmpty()) {
             return "预警附件";
         }
-        return attachmentName.replaceFirst("^\\[(预警报告|处理反馈)\\]\\s*", "");
+        return attachmentName.replaceFirst("^\\[(预警报告|处理反馈|督办附件)\\]\\s*", "");
     }
 
     private String resolveContentType(String savedType, String fileName) {

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

@@ -58,7 +58,7 @@ public class EarlyWarningTodayController {
     public AjaxResult queryTodayPage(
             @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") Integer pageNum,
             @ApiParam(value = "每页大小", defaultValue = "10") @RequestParam(defaultValue = "10") Integer pageSize,
-            @ApiParam(value = "预警状态", allowableValues = "PENDING,PROCESSING,RELEASED,HANDLED,CLOSED") @RequestParam(required = false) String status) {
+            @ApiParam(value = "预警状态", allowableValues = "PENDING,RECEIVED,PROCESSING,HANDLED,CLOSED") @RequestParam(required = false) String status) {
         try {
             Page<EarlyWarning> page = new Page<>(pageNum, pageSize);
             IPage<EarlyWarning> resultPage = earlyWarningTodayService.queryTodayWarningPage(page, status);

+ 1 - 0
pipe-network-service/zksy-common/src/main/java/com/zksy/common/sms/SmsProperties.java

@@ -20,4 +20,5 @@ public class SmsProperties {
     private String regionId;
 
     private String maintainTemplateCode;
+    private String supervisionTemplateCode;
 }

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

@@ -82,6 +82,10 @@ public class EarlyWarning implements Serializable {
     @ApiModelProperty("处置人")
     private String handler;
 
+    @TableField("reviewer")
+    @ApiModelProperty("核查人账号,发布时未指定则使用发布人")
+    private String reviewer;
+
     @TableField("status")
     @ApiModelProperty("状态")
     private String status;

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

@@ -24,6 +24,12 @@ public class WarningAttachment implements Serializable {
     @TableField("warning_id")
     @ApiModelProperty("预警ID")
     private String warningId;
+
+    @TableField("disposal_id")
+    private String disposalId;
+
+    @TableField("attachment_stage")
+    private String attachmentStage;
     @TableField("attachment_name")
     @ApiModelProperty("附件名称")
     private String attachmentName;

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

@@ -27,6 +27,10 @@ public class WarningSupervision implements Serializable {
     private String notificationType;
     @TableField("notification_content")
     private String notificationContent;
+    @TableField("notification_status")
+    private String notificationStatus;
+    @TableField("notification_error")
+    private String notificationError;
     @TableField("attachment_url")
     private String attachmentUrl;
     @TableField("create_by")

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

@@ -37,6 +37,10 @@ public class WarningTodo implements Serializable {
     @ApiModelProperty("待办类型")
     private String todoType;
 
+    @TableField("action_type")
+    @ApiModelProperty("任务职责:HANDLE处置,REVIEW核查")
+    private String actionType;
+
     @TableField("read_status")
     @ApiModelProperty("阅读状态")
     private String readStatus;

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

@@ -0,0 +1,50 @@
+package com.zksy.base.warning.domain;
+
+import com.zksy.common.exception.ServiceException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** 预警业务状态的唯一主线;已处置仍需核查,只有已解除才是终态。 */
+public final class WarningWorkflow {
+    public static final String DRAFT = "DRAFT";
+    public static final String PENDING = "PENDING";
+    public static final String RECEIVED = "RECEIVED";
+    public static final String PROCESSING = "PROCESSING";
+    public static final String HANDLED = "HANDLED";
+    public static final String CLOSED = "CLOSED";
+
+    private WarningWorkflow() { }
+
+    public static String label(String state) {
+        if (state == null) return "未知状态";
+        switch (state) {
+            case DRAFT: return "草稿";
+            case PENDING: return "已发布(待接收)";
+            case RECEIVED: return "已接收";
+            case PROCESSING: return "处置中";
+            case HANDLED: return "已处置(待核查)";
+            case CLOSED: return "已解除";
+            default: return state;
+        }
+    }
+
+    public static void requireState(EarlyWarning warning, String operation, String... allowed) {
+        if (warning == null) throw new ServiceException("预警不存在");
+        if (!Arrays.asList(allowed).contains(warning.getStatus())) {
+            throw new ServiceException("当前预警为" + label(warning.getStatus()) + ",不能" + operation);
+        }
+    }
+
+    public static List<String> actions(String state) {
+        if (state == null) return Collections.emptyList();
+        switch (state) {
+            case DRAFT: return Arrays.asList("EDIT", "DELETE", "PUBLISH");
+            case PENDING: return Arrays.asList("RECEIVE", "UPGRADE", "SUPERVISION", "INSTRUCTION");
+            case RECEIVED: return Arrays.asList("START", "UPGRADE", "SUPERVISION", "INSTRUCTION");
+            case PROCESSING: return Arrays.asList("HANDLE", "UPGRADE", "SUPERVISION", "INSTRUCTION");
+            case HANDLED: return Arrays.asList("CLEAR", "RETURN", "UPGRADE", "SUPERVISION", "INSTRUCTION");
+            default: return Collections.emptyList();
+        }
+    }
+}

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

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zksy.base.warning.domain.EarlyWarning;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -13,6 +14,9 @@ import java.util.Map;
 
 @Mapper
 public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
+    /** 同一预警的发布、处置、核查串行执行,防止重复点击造成状态或待办错乱。 */
+    @Select("SELECT * FROM early_warning WHERE warning_id=#{warningId} FOR UPDATE")
+    EarlyWarning selectForUpdate(@Param("warningId") String warningId);
 
     IPage<Map<String, Object>> selectWarningList(Page<?> page, @Param("warningName") String warningName, @Param("warningType") String warningType, @Param("warningLevel") String warningLevel, @Param("status") String status);
 
@@ -20,6 +24,8 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
 
     IPage<Map<String, Object>> selectTodoList(Page<?> page, @Param("userId") String userId, @Param("todoType") String todoType, @Param("warningName") String warningName);
 
+    IPage<Map<String, Object>> selectMyTasks(Page<?> page, @Param("username") String username, @Param("userId") String userId, @Param("q") Map<String, String> filters);
+
     IPage<Map<String, Object>> selectProcessingWarnings(Page<?> page, @Param("warningType") String warningType, @Param("warningLevel") String warningLevel);
 
     IPage<Map<String, Object>> selectCompletedWarnings(Page<?> page, @Param("warningType") String warningType, @Param("warningLevel") String warningLevel, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);

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

@@ -22,6 +22,10 @@ public interface EarlyWarningService extends IService<EarlyWarning> {
 
     IPage<Map<String, Object>> getTodoList(Page<?> page, String userName, String todoType, String warningName);
 
+    IPage<Map<String, Object>> getTodoList(Page<?> page, Map<String, String> filters);
+
+    boolean uploadReport(String warningId, List<MultipartFile> files);
+
     Map<String, Object> getWarningDetail(String warningId);
 
     Map<String, Object> getWarningFullDetail(String warningId);
@@ -40,12 +44,16 @@ public interface EarlyWarningService extends IService<EarlyWarning> {
      */
     boolean publishWarning(String warningId, String handler);
 
+    boolean publishWarning(String warningId, String handler, String reviewer);
+
     default boolean publishWarning(String warningId) {
         return publishWarning(warningId, null);
     }
 
     boolean confirmWarning(String warningId, String assignedUser, String confirmRemark);
 
+    boolean startWarningProcess(String warningId, String remark);
+
     boolean misreportWarning(String warningId, String reason);
 
     boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files);
@@ -58,7 +66,9 @@ public interface EarlyWarningService extends IService<EarlyWarning> {
 
     boolean returnWarning(String warningId, String disposalContent);
 
-    boolean supervisionWarning(String warningId, String supervisionUser, String supervisionContent, String notificationType, String notificationContent, String attachmentUrl);
+    WarningSupervision supervisionWarning(String warningId, String supervisionUser, String supervisionContent, String notificationType, String notificationContent, MultipartFile attachment);
+
+    boolean readNotification(String todoId);
 
     boolean addLeaderInstruction(String warningId, String leaderName, String instructionContent, String smsReceivers);
 

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

@@ -0,0 +1,45 @@
+package com.zksy.base.warning.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.zksy.base.warning.domain.WarningSupervision;
+import com.zksy.base.warning.mapper.WarningSupervisionMapper;
+import com.zksy.common.sms.SmsProperties;
+import com.zksy.common.sms.SmsTemplate;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.Collections;
+import java.util.Map;
+
+/** 在督办事务提交后发送,失败只更新发送结果,不重复创建督办。 */
+@Service
+public class WarningSupervisionNotifier {
+    private final ObjectProvider<SmsTemplate> sender;
+    private final SmsProperties properties;
+    private final WarningSupervisionMapper mapper;
+
+    public WarningSupervisionNotifier(ObjectProvider<SmsTemplate> sender, SmsProperties properties, WarningSupervisionMapper mapper) {
+        this.sender = sender;
+        this.properties = properties;
+        this.mapper = mapper;
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void send(WarningSupervision supervision, String phone, String warningName) {
+        boolean success = false;
+        try {
+            JSONObject params = new JSONObject();
+            params.put("warningName", warningName);
+            params.put("content", supervision.getNotificationContent());
+            Map<String, Boolean> result = sender.getObject().sendBatchSms(
+                    Collections.singletonList(phone), params.toJSONString(), properties.getSupervisionTemplateCode());
+            success = Boolean.TRUE.equals(result.get(phone));
+        } catch (Exception ignored) {
+            // 不记录手机号、短信正文或供应商可能回显的敏感响应。
+        }
+        supervision.setNotificationStatus(success ? "SENT" : "FAILED");
+        supervision.setNotificationError(success ? "" : "短信发送失败,请检查短信服务及模板配置");
+        mapper.updateById(supervision);
+    }
+}

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

@@ -7,13 +7,23 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.base.warning.domain.*;
 import com.zksy.base.warning.mapper.*;
 import com.zksy.base.warning.service.EarlyWarningService;
+import com.zksy.base.warning.service.WarningSupervisionNotifier;
 import com.zksy.common.core.domain.entity.SysDictData;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.system.mapper.SysUserMapper;
 import com.zksy.common.utils.DictUtils;
 import com.zksy.common.utils.SecurityUtils;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.sms.SmsProperties;
+import com.zksy.common.sms.SmsTemplate;
 import com.zksy.service.MinioFileStorageService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.time.LocalDateTime;
@@ -21,6 +31,7 @@ import java.time.format.DateTimeFormatter;
 import java.util.*;
 
 @Service
+@Slf4j
 public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, EarlyWarning>
         implements EarlyWarningService {
 
@@ -42,6 +53,14 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     private WarningArchiveMapper warningArchiveMapper;
     @Autowired
     private MinioFileStorageService minioFileStorageService;
+    @Autowired
+    private SysUserMapper sysUserMapper;
+    @Autowired
+    private ObjectProvider<SmsTemplate> smsTemplateProvider;
+    @Autowired
+    private SmsProperties smsProperties;
+    @Autowired
+    private WarningSupervisionNotifier supervisionNotifier;
 
     @Override
     public IPage<Map<String, Object>> getWarningList(Page<?> page, String warningName, String warningType, String warningLevel, String status) {
@@ -55,13 +74,29 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
 
     @Override
     public IPage<Map<String, Object>> getTodoList(Page<?> page, String userName, String todoType, String warningName) {
-        return earlyWarningMapper.selectTodoList(page, userName, todoType, warningName);
+        Map<String, String> filters = new HashMap<>();
+        filters.put("todoType", todoType);
+        filters.put("warningName", warningName);
+        return getTodoList(page, filters);
+    }
+
+    @Override
+    public IPage<Map<String, Object>> getTodoList(Page<?> page, Map<String, String> filters) {
+        String type = filters.get("todoType");
+        if (!isBlank(type) && !Arrays.asList("TODO", "DONE", "NOTICE").contains(type)) {
+            throw new ServiceException("待办类型只能为 TODO、DONE 或 NOTICE");
+        }
+        IPage<Map<String, Object>> result = earlyWarningMapper.selectMyTasks(page, currentUser(),
+                String.valueOf(SecurityUtils.getUserId()), filters);
+        result.getRecords().forEach(this::addWorkflowActions);
+        return result;
     }
 
     @Override
     public Map<String, Object> getWarningDetail(String warningId) {
         Map<String, Object> detail = earlyWarningMapper.selectWarningDetail(warningId);
         if (detail != null) {
+            addWorkflowActions(detail);
             detail.put("attachmentList", getAttachmentList(warningId));
             detail.put("disposalList", getDisposalList(warningId));
             detail.put("alarmList", getLinkedAlarms(warningId));
@@ -78,12 +113,14 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         }
 
         result.put("basicInfo", basicInfo);
+        addWorkflowActions(basicInfo);
         result.put("gisLocation", getGisLocation(basicInfo));
         result.put("disposalHistory", getDisposalHistory(warningId));
         result.put("attachmentList", getAttachmentList(warningId));
         result.put("supervisionList", getSupervisionList(warningId));
         result.put("alarmList", getLinkedAlarms(warningId));
-        result.put("electronicArchive", generateElectronicArchive(basicInfo));
+        // 查询详情不能创建归档,只有核查解除后才生成最终档案。
+        result.put("electronicArchive", getArchiveByWarningId(warningId));
 
         return result;
     }
@@ -149,7 +186,15 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean saveWarning(EarlyWarning warning, List<String> alarmIds, List<MultipartFile> files) {
-        String warningNo = "WJ" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        warning.setWarningId(null);
+        warning.setPublisher(null);
+        warning.setPublishTime(null);
+        warning.setProcessInstanceId(null);
+        // 草稿可预选核查人;未选择时在发布时使用当前发布人。
+        warning.setCreateBy(currentUser());
+        warning.setUpdateBy(currentUser());
+        String warningNo = "YJ" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))
+                + UUID.randomUUID().toString().substring(0, 6).toUpperCase(Locale.ROOT);
         warning.setWarningNo(warningNo);
         warning.setStatus("DRAFT");
         warning.setCreateTime(LocalDateTime.now());
@@ -174,11 +219,22 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         saveAttachments(warningId, null, "REPORT", files);
     }
 
-    private void saveAttachments(String warningId, String disposalId, String stage, List<MultipartFile> files) {
+    private List<WarningAttachment> saveAttachments(String warningId, String disposalId, String stage, List<MultipartFile> files) {
+        List<WarningAttachment> saved = new ArrayList<>();
         try {
             for (MultipartFile file : files) {
                 validateAttachment(file);
                 String path = minioFileStorageService.uploadFile(file, "warning");
+                if (TransactionSynchronizationManager.isSynchronizationActive()) {
+                    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                        @Override public void afterCompletion(int status) {
+                            if (status == STATUS_ROLLED_BACK) {
+                                try { minioFileStorageService.deleteFile(path); }
+                                catch (Exception ignored) { log.warn("预警附件事务回滚后的文件清理失败"); }
+                            }
+                        }
+                    });
+                }
                 // 上传文件
                 String fileName = file.getOriginalFilename();
                 if (fileName == null || fileName.trim().isEmpty()) {
@@ -187,14 +243,19 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
                 // 保存附件信息
                 WarningAttachment attachment = new WarningAttachment();
                 attachment.setWarningId(warningId);
-                attachment.setAttachmentName(("PROCESS".equals(stage) ? "[处理反馈] " : "[预警报告] ") + fileName);
+                attachment.setDisposalId(disposalId);
+                attachment.setAttachmentStage(stage);
+                attachment.setCreateBy(currentUser());
+                attachment.setAttachmentName(("PROCESS".equals(stage) ? "[处理反馈] " : "SUPERVISION".equals(stage) ? "[督办附件] " : "[预警报告] ") + fileName);
                 attachment.setAttachmentUrl(path);
                 // 部分 MIME(如 vnd.openxmlformats-officedocument.*)长度超过 64,写入前按 DB 列宽截断兜底
                 attachment.setAttachmentType(truncateAttachmentType(file.getContentType()));
                 attachment.setAttachmentSize(file.getSize());
                 attachment.setCreateTime(LocalDateTime.now());
                 warningAttachmentMapper.insert(attachment);
+                saved.add(attachment);
             }
+            return saved;
         } catch (Exception e) {
             throw new RuntimeException("附件保存失败", e);
         }
@@ -203,6 +264,16 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean updateWarning(EarlyWarning warning, List<String> alarmIds, List<MultipartFile> files) {
+        EarlyWarning existing = earlyWarningMapper.selectForUpdate(warning.getWarningId());
+        WarningWorkflow.requireState(existing, "修改草稿", WarningWorkflow.DRAFT);
+        warning.setStatus(existing.getStatus());
+        warning.setWarningNo(existing.getWarningNo());
+        warning.setCreateBy(existing.getCreateBy());
+        warning.setCreateTime(existing.getCreateTime());
+        warning.setPublisher(existing.getPublisher());
+        warning.setPublishTime(existing.getPublishTime());
+        warning.setProcessInstanceId(existing.getProcessInstanceId());
+        warning.setUpdateBy(currentUser());
         warning.setUpdateTime(LocalDateTime.now());
         boolean result = updateById(warning);
         if (result) {
@@ -219,6 +290,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean deleteWarning(String warningId) {
+        WarningWorkflow.requireState(earlyWarningMapper.selectForUpdate(warningId), "删除", WarningWorkflow.DRAFT);
         // 先查询附件列表,删除物理文件
         List<WarningAttachment> attachments = warningAttachmentMapper.selectList(
                 new LambdaQueryWrapper<WarningAttachment>()
@@ -279,14 +351,27 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean publishWarning(String warningId, String requestedHandler) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null) return false;
+        return publishWarning(warningId, requestedHandler, null);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean publishWarning(String warningId, String requestedHandler, String requestedReviewer) {
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "发布", WarningWorkflow.DRAFT);
 
         String assignee = isBlank(requestedHandler) ? warning.getHandler() : requestedHandler.trim();
         // 发布不能再自动指派当前登录用户,处置人必须由发布人明确选择或提前保存。
-        if (isBlank(assignee)) return false;
+        requireActiveUser(assignee, "处置人");
+        String reviewer = isBlank(requestedReviewer)
+                ? (isBlank(warning.getReviewer()) ? currentUser() : warning.getReviewer().trim())
+                : requestedReviewer.trim();
+        requireActiveUser(reviewer, "核查人");
+        warning.setReviewer(reviewer);
 
         warning.setStatus("PENDING");
+        warning.setPublisher(currentUser());
+        warning.setUpdateBy(currentUser());
         warning.setPublishTime(LocalDateTime.now());
         warning.setUpdateTime(LocalDateTime.now());
         warning.setHandler(assignee);
@@ -294,8 +379,9 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         boolean result = updateById(warning);
 
         if (result) {
-            addDisposalRecord(warningId, "RELEASE", null, null, assignee, "系统发布预警信息");
-            createTodo(warningId, assignee, "待办预警", "TODO");
+            addDisposalRecord(warningId, "RELEASE", null, null, currentUser(),
+                    "发布预警;处置人:" + assignee + ";核查人:" + reviewer);
+            createTodo(warningId, assignee, "接收并处置预警", "TODO", "HANDLE");
         }
 
         return result;
@@ -304,44 +390,54 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean confirmWarning(String warningId, String assignedUser, String confirmRemark) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null || "CLOSED".equals(warning.getStatus())) return false;
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "接收", WarningWorkflow.PENDING);
+        requireHandler(warning);
+        if (!isBlank(assignedUser) && !Objects.equals(warning.getHandler(), assignedUser.trim())) {
+            throw new ServiceException("接收时不能变更发布人指定的处置人");
+        }
         LocalDateTime now = LocalDateTime.now();
         String operator = currentUser();
-        String assignee = isBlank(assignedUser) ? warning.getHandler() : assignedUser;
-        if (isBlank(assignee)) assignee = operator;
-        warning.setStatus("PROCESSING");
+        String assignee = warning.getHandler();
+        // 接收预警:待办(PENDING) → 已接收(RECEIVED)
+        warning.setStatus("RECEIVED");
         warning.setHandler(assignee);
-        warning.setPublishTime(warning.getPublishTime() == null ? now : warning.getPublishTime());
         warning.setUpdateTime(now);
         if (!updateById(warning)) return false;
-        addDisposalRecord(warningId, "CONFIRM", null, null, operator, confirmRemark);
-        createTodo(warningId, assignee, "预警处理", "TODO", "PROCESS");
+        addDisposalRecord(warningId, "RECEIVE", null, null, operator, confirmRemark);
         return true;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean misreportWarning(String warningId, String reason) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null) return false;
+    public boolean startWarningProcess(String warningId, String remark) {
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "开始处置", WarningWorkflow.RECEIVED);
+        requireHandler(warning);
         LocalDateTime now = LocalDateTime.now();
         String operator = currentUser();
-        warning.setStatus("CLOSED");
-        warning.setRemark(reason);
+        // 开始处置:已接收(RECEIVED) → 处置中(PROCESSING)
+        warning.setStatus("PROCESSING");
         warning.setUpdateTime(now);
         if (!updateById(warning)) return false;
-        addDisposalRecord(warningId, "MISREPORT", null, null, operator, reason);
-        completeTodo(warningId);
-        generateArchive(warningId);
+        addDisposalRecord(warningId, "START", null, null, operator, remark);
         return true;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean misreportWarning(String warningId, String reason) {
+        if (isBlank(reason)) throw new ServiceException("请填写误报核查依据");
+        return clearWarning(warningId, "误报核查:" + reason.trim());
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null || "CLOSED".equals(warning.getStatus())) return false;
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "提交处置结果", WarningWorkflow.PROCESSING);
+        requireHandler(warning);
+        if (isBlank(processContent)) throw new ServiceException("请填写现场处置结果");
         LocalDateTime now = LocalDateTime.now();
         String operator = currentUser();
         warning.setStatus("HANDLED");
@@ -359,14 +455,17 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
             saveAttachments(warningId, disposal.getDisposalId(), "PROCESS", files);
         }
         completeTodo(warningId);
+        createTodo(warningId, reviewerOf(warning), "核查处置结果", "TODO", "REVIEW");
         return true;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean clearWarning(String warningId, String clearRemark) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null || "CLOSED".equals(warning.getStatus())) return false;
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "核查解除", WarningWorkflow.HANDLED);
+        requireReviewer(warning);
+        if (isBlank(clearRemark)) throw new ServiceException("请填写核查通过和解除依据");
         LocalDateTime now = LocalDateTime.now();
         String operator = currentUser();
         warning.setStatus("CLOSED");
@@ -385,23 +484,80 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     }
 
     private String currentUser() {
-        try {
-            String username = SecurityUtils.getUsername();
-            return isBlank(username) ? "system" : username;
-        } catch (Exception ex) {
-            return "system";
-        }
+        String username = SecurityUtils.getUsername();
+        if (isBlank(username)) throw new ServiceException("请登录后操作预警");
+        return username;
     }
 
     private boolean isBlank(String value) {
         return value == null || value.trim().isEmpty();
     }
 
+    private void requireHandler(EarlyWarning warning) {
+        if (!Objects.equals(warning.getHandler(), currentUser())) {
+            throw new ServiceException("只有发布时指定的处置人可以接收和提交处置结果");
+        }
+    }
+
+    private String reviewerOf(EarlyWarning warning) {
+        return isBlank(warning.getReviewer()) ? warning.getPublisher() : warning.getReviewer();
+    }
+
+    private void requireReviewer(EarlyWarning warning) {
+        if (!Objects.equals(reviewerOf(warning), currentUser())) {
+            throw new ServiceException("只有指定核查人可以解除或退回预警");
+        }
+    }
+
+    private SysUser requireActiveUser(String username, String role) {
+        if (isBlank(username)) throw new ServiceException("请选择" + role);
+        SysUser user = sysUserMapper.selectUserByUserName(username.trim());
+        if (user == null || !"0".equals(user.getStatus()) || !"0".equals(user.getDelFlag())) {
+            throw new ServiceException(role + "必须是有效且启用的系统用户");
+        }
+        return user;
+    }
+
+    private void addWorkflowActions(Map<String, Object> row) {
+        String state = Objects.toString(row.get("status"), "");
+        String username = currentUser();
+        String reviewer = Objects.toString(row.get("reviewer"), "");
+        if (isBlank(reviewer)) reviewer = Objects.toString(row.get("publisher"), "");
+        List<String> actions = new ArrayList<>(WarningWorkflow.actions(state));
+        if (!Objects.equals(username, row.get("handler"))) {
+            actions.removeAll(Arrays.asList("RECEIVE", "START", "HANDLE"));
+        }
+        if (!username.equals(reviewer)) actions.removeAll(Arrays.asList("CLEAR", "RETURN"));
+        if ("DONE".equals(row.get("todo_type"))) actions.clear();
+        row.put("availableActions", actions);
+        row.put("statusLabel", WarningWorkflow.label(state));
+        row.put("reviewer", reviewer);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean uploadReport(String warningId, List<MultipartFile> files) {
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "上传报告", WarningWorkflow.DRAFT, WarningWorkflow.PENDING,
+                WarningWorkflow.RECEIVED, WarningWorkflow.PROCESSING, WarningWorkflow.HANDLED);
+        if (files == null || files.isEmpty()) throw new ServiceException("请选择预警报告文件");
+        saveAttachments(warningId, null, "REPORT", files);
+        addDisposalRecord(warningId, "REPORT", null, null, currentUser(), "上传预警报告,共" + files.size() + "个附件");
+        return true;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean upgradeWarning(String warningId, String newLevel, String disposalContent) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null) return false;
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "升级", WarningWorkflow.PENDING, WarningWorkflow.RECEIVED,
+                WarningWorkflow.PROCESSING, WarningWorkflow.HANDLED);
+        if (isBlank(disposalContent)) throw new ServiceException("请填写会商研判意见和升级依据");
+        if (newLevel == null || !newLevel.matches("[1-4]")
+                || warning.getWarningLevel() == null || !warning.getWarningLevel().matches("[1-4]")
+                || Integer.parseInt(newLevel) >= Integer.parseInt(warning.getWarningLevel())) {
+            throw new ServiceException("只能升级至更严重的级别,一级为最高级");
+        }
 
         String oldLevel = warning.getWarningLevel();
         warning.setWarningLevel(newLevel);
@@ -409,7 +565,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
 
         boolean result = updateById(warning);
         if (result) {
-            addDisposalRecord(warningId, "UPGRADE", oldLevel, newLevel, warning.getHandler(), disposalContent);
+            addDisposalRecord(warningId, "UPGRADE", oldLevel, newLevel, currentUser(), disposalContent);
         }
         return result;
     }
@@ -417,80 +573,112 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean resolveWarning(String warningId, String disposalContent) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null) return false;
-
-        warning.setStatus("CLOSED");
-        warning.setUpdateTime(LocalDateTime.now());
-
-        boolean result = updateById(warning);
-        if (result) {
-            addDisposalRecord(warningId, "RESOLVE", null, null, warning.getHandler(), disposalContent);
-            completeTodo(warningId);
-            
-            // 解除预警时自动创建归档
-            Map<String, Object> warningDetail = earlyWarningMapper.selectWarningDetail(warningId);
-            if (warningDetail != null) {
-                generateElectronicArchive(warningDetail);
-            }
-        }
-        return result;
+        return clearWarning(warningId, disposalContent);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean returnWarning(String warningId, String disposalContent) {
-        EarlyWarning warning = getById(warningId);
-        if (warning == null) return false;
-
-        warning.setStatus("DRAFT");
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "退回重办", WarningWorkflow.HANDLED);
+        requireReviewer(warning);
+        if (isBlank(disposalContent)) throw new ServiceException("请填写退回原因和重新处置要求");
+        requireActiveUser(warning.getHandler(), "原处置人");
+        warning.setStatus("PROCESSING");
+        warning.setUpdateBy(currentUser());
         warning.setUpdateTime(LocalDateTime.now());
 
         boolean result = updateById(warning);
         if (result) {
-            addDisposalRecord(warningId, "RETURN", null, null, warning.getHandler(), disposalContent);
-            // 退回后删除待办记录,因为直接回到草稿状态
-            LambdaQueryWrapper<WarningTodo> todoWrapper = new LambdaQueryWrapper<>();
-            todoWrapper.eq(WarningTodo::getWarningId, warningId);
-            warningTodoMapper.delete(todoWrapper);
+            addDisposalRecord(warningId, "RETURN", null, null, currentUser(), disposalContent);
+            completeTodo(warningId);
+            createTodo(warningId, warning.getHandler(), "核查退回,重新处置", "TODO", "HANDLE");
         }
         return result;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean supervisionWarning(String warningId, String supervisionUser, String supervisionContent, String notificationType, String notificationContent, String attachmentUrl) {
+    public WarningSupervision supervisionWarning(String warningId, String supervisionUser, String supervisionContent, String notificationType, String notificationContent, MultipartFile attachment) {
+        EarlyWarning warning = earlyWarningMapper.selectForUpdate(warningId);
+        WarningWorkflow.requireState(warning, "督办",
+                WarningWorkflow.PENDING, WarningWorkflow.RECEIVED, WarningWorkflow.PROCESSING, WarningWorkflow.HANDLED);
+        if (isBlank(supervisionUser) || isBlank(supervisionContent)) throw new ServiceException("请填写督办人员和督办意见");
+        if (!"短信".equals(notificationType) && !"系统通知".equals(notificationType)) {
+            throw new ServiceException("督办通知方式仅支持短信或系统通知");
+        }
+        SysUser recipient = requireActiveUser(supervisionUser, "督办人员");
+        String message = isBlank(notificationContent) ? supervisionContent.trim() : notificationContent.trim();
+        if (message.length() > 180) throw new ServiceException("通知内容不能超过180字");
+        if (attachment != null) validateAttachment(attachment);
+        if ("短信".equals(notificationType)) {
+            if (isBlank(recipient.getPhonenumber())) throw new ServiceException("督办人员未配置手机号");
+            if (smsTemplateProvider.getIfAvailable() == null || isBlank(smsProperties.getSupervisionTemplateCode())) {
+                throw new ServiceException("督办短信服务或模板未配置");
+            }
+        }
         WarningSupervision supervision = new WarningSupervision();
         supervision.setWarningId(warningId);
-        supervision.setSupervisionUser(supervisionUser);
+        supervision.setSupervisionUser(recipient.getUserName());
         supervision.setSupervisionContent(supervisionContent);
         supervision.setNotificationType(notificationType);
-        supervision.setNotificationContent(notificationContent);
-        supervision.setAttachmentUrl(attachmentUrl);
+        supervision.setNotificationContent(message);
+        supervision.setNotificationStatus("系统通知".equals(notificationType) ? "SENT" : "PENDING");
         supervision.setCreateBy(currentUser());
         supervision.setCreateTime(LocalDateTime.now());
-        warningSupervisionMapper.insert(supervision);
         WarningDisposal disposal = new WarningDisposal();
         disposal.setWarningId(warningId);
         disposal.setDisposalType("SUPERVISION");
-        disposal.setDisposalUser(supervisionUser);
+        disposal.setDisposalUser(currentUser());
         disposal.setDisposalContent(supervisionContent);
-        disposal.setSupervisionUser(supervisionUser);
+        disposal.setSupervisionUser(recipient.getUserName());
         disposal.setSupervisionContent(supervisionContent);
         disposal.setNotificationType(notificationType);
         disposal.setCreateTime(LocalDateTime.now());
 
-        return warningDisposalMapper.insert(disposal) > 0;
+        if (warningDisposalMapper.insert(disposal) <= 0) throw new ServiceException("督办保存失败");
+        if (attachment != null) {
+            List<WarningAttachment> files = saveAttachments(warningId, disposal.getDisposalId(), "SUPERVISION", Collections.singletonList(attachment));
+            supervision.setAttachmentUrl(files.get(0).getAttachmentUrl());
+        }
+        if (warningSupervisionMapper.insert(supervision) <= 0) throw new ServiceException("督办保存失败");
+        if ("系统通知".equals(notificationType)) {
+            createTodo(warningId, recipient.getUserName(), message, "NOTICE", "NOTICE");
+        } else {
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override public void afterCommit() {
+                    try { supervisionNotifier.send(supervision, recipient.getPhonenumber(), warning.getWarningName()); }
+                    catch (Exception ignored) {
+                        supervision.setNotificationStatus("FAILED");
+                        supervision.setNotificationError("短信发送结果未能确认,请联系管理员核查,勿重复提交");
+                        log.warn("督办短信发送结果保存失败,supervisionId={}", supervision.getSupervisionId());
+                    }
+                }
+            });
+        }
+        return supervision;
+    }
+
+    @Override
+    public boolean readNotification(String todoId) {
+        WarningTodo todo = warningTodoMapper.selectById(todoId);
+        if (todo == null || !"NOTICE".equals(todo.getTodoType())
+                || !Objects.equals(String.valueOf(SecurityUtils.getUserId()), todo.getUserId())) return false;
+        todo.setReadStatus("READ");
+        return warningTodoMapper.updateById(todo) > 0;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean addLeaderInstruction(String warningId, String leaderName, String instructionContent, String smsReceivers) {
+        WarningWorkflow.requireState(earlyWarningMapper.selectForUpdate(warningId), "添加领导批示",
+                WarningWorkflow.PENDING, WarningWorkflow.RECEIVED, WarningWorkflow.PROCESSING, WarningWorkflow.HANDLED);
+        if (isBlank(leaderName) || isBlank(instructionContent)) throw new ServiceException("请填写批示领导和批示内容");
         WarningDisposal disposal = new WarningDisposal();
         disposal.setWarningId(warningId);
         disposal.setDisposalType("INSTRUCTION");
-        disposal.setDisposalUser(leaderName);
-        disposal.setDisposalContent(instructionContent);
+        disposal.setDisposalUser(currentUser());
+        disposal.setDisposalContent("批示领导:" + leaderName + ";" + instructionContent);
         disposal.setCreateTime(LocalDateTime.now());
 
         return warningDisposalMapper.insert(disposal) > 0;
@@ -499,12 +687,15 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean linkAlarms(String warningId, List<String> alarmIds) {
+        WarningWorkflow.requireState(earlyWarningMapper.selectForUpdate(warningId), "关联报警",
+                WarningWorkflow.DRAFT, WarningWorkflow.PENDING, WarningWorkflow.RECEIVED, WarningWorkflow.PROCESSING, WarningWorkflow.HANDLED);
         LambdaQueryWrapper<AlarmWarningRel> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AlarmWarningRel::getWarningId, warningId);
         alarmWarningRelMapper.delete(wrapper);
 
         if (alarmIds != null && !alarmIds.isEmpty()) {
-            for (String alarmId : alarmIds) {
+            for (String alarmId : new LinkedHashSet<>(alarmIds)) {
+                if (isBlank(alarmId)) throw new ServiceException("报警编号不能为空");
                 AlarmWarningRel rel = new AlarmWarningRel();
                 rel.setWarningId(warningId);
                 rel.setAlarmId(alarmId);
@@ -673,8 +864,11 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
             map.put("attachmentType", attachment.getAttachmentType());
             map.put("attachmentSize", attachment.getAttachmentSize());
             map.put("createTime", attachment.getCreateTime());
+            map.put("createBy", attachment.getCreateBy());
+            map.put("disposalId", attachment.getDisposalId());
             String attachmentName = attachment.getAttachmentName();
-            map.put("attachmentStage", attachmentName != null && attachmentName.startsWith("[处理反馈]") ? "PROCESS" : "REPORT");
+            map.put("attachmentStage", isBlank(attachment.getAttachmentStage())
+                    ? (attachmentName != null && attachmentName.startsWith("[处理反馈]") ? "PROCESS" : "REPORT") : attachment.getAttachmentStage());
             result.add(map);
         }
         return result;
@@ -690,10 +884,12 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     }
 
     private void createTodo(String warningId, String userId, String taskName, String todoType, String actionType) {
+        SysUser user = requireActiveUser(userId, "待办接收人");
         WarningTodo todo = new WarningTodo();
         todo.setWarningId(warningId);
-        todo.setUserId(userId);
-        todo.setUserName(userId);
+        todo.setUserId(String.valueOf(user.getUserId()));
+        todo.setUserName(user.getUserName());
+        todo.setActionType(actionType);
         todo.setTodoType(todoType);
         todo.setReadStatus("UNREAD");
         todo.setTaskName(taskName);
@@ -754,6 +950,11 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     private static final Map<String, String> DISPOSAL_TYPE_MAP = new HashMap<>();
     static {
         DISPOSAL_TYPE_MAP.put("RELEASE", "发布预警");
+        DISPOSAL_TYPE_MAP.put("RECEIVE", "接收预警");
+        DISPOSAL_TYPE_MAP.put("START", "开始处置");
+        DISPOSAL_TYPE_MAP.put("HANDLE", "提交处置结果,待核查");
+        DISPOSAL_TYPE_MAP.put("CLEAR", "核查通过并解除");
+        DISPOSAL_TYPE_MAP.put("REPORT", "上传预警报告");
         DISPOSAL_TYPE_MAP.put("UPGRADE", "升级预警");
         DISPOSAL_TYPE_MAP.put("RESOLVE", "解除预警");
         DISPOSAL_TYPE_MAP.put("RETURN", "退回重办");
@@ -826,16 +1027,17 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
 
         String status = warning.getStatus();
         String currentStatus = "waiting";
-        if ("CLOSED".equals(status) || "HANDLED".equals(status)) {
+        if ("CLOSED".equals(status)) {
             currentStatus = "completed";
-        } else if ("PROCESSING".equals(status) || "PENDING".equals(status) || "RELEASED".equals(status)) {
+        } else if (!"DRAFT".equals(status)) {
             currentStatus = "active";
         }
 
         Map<String, Object> endNode = new HashMap<>();
         endNode.put("id", "end");
-        endNode.put("name", "处置完成");
-        endNode.put("type", "endEvent");
+        endNode.put("name", "CLOSED".equals(status) ? "核查解除,流程结束" : WarningWorkflow.label(status));
+        endNode.put("type", "CLOSED".equals(status) ? "endEvent" : "userTask");
+        endNode.put("disposalUser", "HANDLED".equals(status) ? reviewerOf(warning) : warning.getHandler());
         endNode.put("x", x);
         endNode.put("y", 150);
         endNode.put("status", currentStatus);
@@ -881,8 +1083,10 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         result.put("supervisionUser", disposal.getSupervisionUser());
         result.put("supervisionContent", disposal.getSupervisionContent());
         result.put("notificationType", disposal.getNotificationType());
-        result.put("createTime", disposal.getCreateTime() != null ? 
+        result.put("createTime", disposal.getCreateTime() != null ?
                 disposal.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null);
+        result.put("attachmentList", warningAttachmentMapper.selectList(
+                new LambdaQueryWrapper<WarningAttachment>().eq(WarningAttachment::getDisposalId, disposalId)));
 
         return result;
     }

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/BusinessCockpitEventAdapter.java

@@ -57,7 +57,7 @@ public class BusinessCockpitEventAdapter {
     private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
     private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
     private static final List<String> CURRENT_WARNING_STATUSES = java.util.Arrays.asList(
-            "PENDING", "PROCESSING", "HANDLING", "RELEASED");
+            "PENDING", "RECEIVED", "PROCESSING", "HANDLING");
 
     private final EquipmentBaseService equipmentBaseService;
     private final EquipmentTypeService equipmentTypeService;

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

@@ -91,7 +91,7 @@ public class DrainageWarning {
     @TableField(exist = false)
     private Long scopeDeptId;
 
-    @ApiModelProperty("状态(DRAFT草稿,PENDING待办,PROCESSING处置中,RELEASED已发布,HANDLED已处置,CLOSED已解除)")
+    @ApiModelProperty("状态(DRAFT草稿,PENDING待办,RECEIVED已接收,PROCESSING处置中,HANDLED已处置,CLOSED已解除)")
     private String status;
 
     @ApiModelProperty("处理人ID")

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

@@ -28,7 +28,7 @@ public interface DrainageWarningService extends IService<DrainageWarning> {
                                      String status, String startTime, String endTime);
 
     /**
-     * 查询当前预警(status IN DRAFT/PENDING/PROCESSING/RELEASED)
+     * 查询当前预警(status IN DRAFT/PENDING/RECEIVED/PROCESSING)
      */
     Page<DrainageWarning> findCurrentPage(long pageNum, long pageSize,
                                           Integer warningLevel, String warningType);
@@ -58,7 +58,7 @@ public interface DrainageWarningService extends IService<DrainageWarning> {
      * 更新预警状态
      *
      * @param id        预警ID
-     * @param status    新状态(字符串:DRAFT/PENDING/PROCESSING/RELEASED/HANDLED/CLOSED)
+     * @param status    新状态(字符串:DRAFT/PENDING/RECEIVED/PROCESSING/HANDLED/CLOSED)
      * @param processor 处理人
      */
     boolean updateStatus(String id, String status, String processor);
@@ -82,7 +82,7 @@ public interface DrainageWarningService extends IService<DrainageWarning> {
      * 查询当前用户可见的预警(按用户ID和部门ID过滤)
      * - admin:看所有预警
      * - 普通用户:看 publish_scope=2 且部门匹配 或 publish_scope=3 且被指定的预警
-     * - currentOnly=true 时限定 status IN ('DRAFT','PENDING','PROCESSING','RELEASED')
+     * - currentOnly=true 时限定 status IN ('DRAFT','PENDING','RECEIVED','PROCESSING')
      * - status 参数用于前端 Tab 筛选,为 null 时返回所有符合的状态
      *
      * @param pageNum       页码

+ 2 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageWarningServiceImpl.java

@@ -42,7 +42,7 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
     @Autowired
     private EquipmentBaseMapper equipmentBaseMapper;
 
-    private static final List<String> CURRENT_STATUSES = Arrays.asList("DRAFT", "PENDING", "PROCESSING", "RELEASED");
+    private static final List<String> CURRENT_STATUSES = Arrays.asList("DRAFT", "PENDING", "RECEIVED", "PROCESSING");
     private static final List<String> HISTORY_STATUSES = Arrays.asList("HANDLED", "CLOSED");
 
     @Override
@@ -62,7 +62,7 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
                                                   Integer warningLevel, String warningType) {
         Page<DrainageWarning> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<DrainageWarning> wrapper = new LambdaQueryWrapper<>();
-        // 当前预警:status IN ('DRAFT','PENDING','PROCESSING','RELEASED')
+        // 当前预警:status IN ('DRAFT','PENDING','RECEIVED','PROCESSING')
         wrapper.in(DrainageWarning::getStatus, CURRENT_STATUSES);
         // 附加筛选条件
         if (warningLevel != null) {

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

@@ -13,6 +13,7 @@
              WHERE e.equipment_code = a.device_code ORDER BY e.equipment_id LIMIT 1) AS device_type,
             a.warning_type,
             a.warning_code,
+            a.alarm_no,
             a.actual_value,
             a.alarm_status,
             a.create_time

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

@@ -18,6 +18,7 @@
         <result column="publisher" property="publisher"/>
         <result column="publish_time" property="publishTime"/>
         <result column="handler" property="handler"/>
+        <result column="reviewer" property="reviewer"/>
         <result column="status" property="status"/>
         <result column="warning_content" property="warningContent"/>
         <result column="process_instance_id" property="processInstanceId"/>
@@ -31,7 +32,7 @@
     <sql id="Base_Column_List">
         warning_id, warning_no, warning_name, warning_type, warning_level, warning_special,
         device_code, device_name,
-        location, longitude, latitude, ownership_unit, publisher, publish_time, handler,
+        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
     </sql>
@@ -784,7 +785,7 @@
         ) open_todo ON open_todo.warning_id = w.warning_id
         <where>
             AND w.publish_time &lt;= CURRENT_TIMESTAMP
-            AND w.status IN ('PENDING', 'PROCESSING', 'RELEASED')
+            AND w.status IN ('PENDING', 'RECEIVED', 'PROCESSING')
             <include refid="SupportedDashboardWarningTypes"/>
             <include refid="DashboardWarningTypeFilter"/>
             <include refid="DashboardWarningLevelFilter"/>
@@ -827,7 +828,7 @@
         FROM app_user.early_warning w
         <where>
             AND w.publish_time &lt;= CURRENT_TIMESTAMP
-            AND w.status IN ('PENDING', 'PROCESSING', 'RELEASED')
+            AND w.status IN ('PENDING', 'RECEIVED', 'PROCESSING')
             <include refid="SupportedDashboardWarningTypes"/>
             <include refid="DashboardWarningTypeFilter"/>
             <include refid="DashboardWarningLevelFilter"/>
@@ -935,11 +936,49 @@
         ORDER BY t.create_time DESC
     </select>
 
+    <!-- 身份由服务端登录上下文传入,禁止使用请求中的 userName。 -->
+    <select id="selectMyTasks" resultType="java.util.Map">
+        SELECT w.*, t.todo_id, t.todo_type, t.action_type, t.task_name, t.read_status,
+               t.create_time AS task_create_time, t.complete_time,
+               (SELECT STRING_AGG(DISTINCT a.alarm_no, ',')
+                FROM alarm_warning_rel rel JOIN alarm_data a ON a.id = rel.alarm_id
+                WHERE rel.warning_id = w.warning_id) AS alarm_numbers
+        FROM (
+            SELECT task.*, ROW_NUMBER() OVER (
+                  PARTITION BY CASE WHEN task.todo_type = 'NOTICE' THEN task.todo_id ELSE task.warning_id END
+                ORDER BY task.complete_time DESC NULLS LAST, task.create_time DESC, task.todo_id DESC
+            ) AS task_rank
+            FROM warning_todo task
+            WHERE (task.user_name = #{username} OR task.user_id = #{userId}
+                   OR (task.user_name IS NULL AND task.user_id = #{username}))
+              <if test="q.todoType != null and q.todoType != ''">AND task.todo_type = #{q.todoType}</if>
+              <if test="q.todoType == null or q.todoType == ''">AND task.todo_type IN ('TODO', 'DONE')</if>
+        ) t JOIN early_warning w ON w.warning_id = t.warning_id
+        WHERE t.task_rank = 1
+        <if test="q.warningName != null and q.warningName != ''">AND w.warning_name LIKE CONCAT('%',#{q.warningName},'%')</if>
+        <if test="q.warningNo != null and q.warningNo != ''">AND w.warning_no LIKE CONCAT('%',#{q.warningNo},'%')</if>
+        <if test="q.warningSpecial != null and q.warningSpecial != ''">AND w.warning_special LIKE CONCAT('%',#{q.warningSpecial},'%')</if>
+        <if test="q.warningType != null and q.warningType != ''">AND w.warning_type = #{q.warningType}</if>
+        <if test="q.warningLevel != null and q.warningLevel != ''">AND w.warning_level = #{q.warningLevel}</if>
+        <if test="q.status != null and q.status != ''">AND w.status = #{q.status}</if>
+        <if test="q.handler != null and q.handler != ''">AND w.handler LIKE CONCAT('%',#{q.handler},'%')</if>
+        <if test="q.publisher != null and q.publisher != ''">AND w.publisher LIKE CONCAT('%',#{q.publisher},'%')</if>
+        <if test="q.location != null and q.location != ''">AND w.location LIKE CONCAT('%',#{q.location},'%')</if>
+        <if test="q.ownershipUnit != null and q.ownershipUnit != ''">AND w.ownership_unit LIKE CONCAT('%',#{q.ownershipUnit},'%')</if>
+        <if test="q.startTime != null and q.startTime != ''">AND w.publish_time &gt;= CAST(#{q.startTime} AS TIMESTAMP)</if>
+        <if test="q.endTime != null and q.endTime != ''">AND w.publish_time &lt;= CAST(#{q.endTime} AS TIMESTAMP)</if>
+        <if test="q.alarmCode != null and q.alarmCode != ''">
+            AND EXISTS (SELECT 1 FROM alarm_warning_rel r JOIN alarm_data a ON a.id = r.alarm_id
+                        WHERE r.warning_id = w.warning_id AND a.alarm_no LIKE CONCAT('%',#{q.alarmCode},'%'))
+        </if>
+        ORDER BY t.create_time DESC, t.todo_id DESC
+    </select>
+
     <select id="selectProcessingWarnings" resultType="java.util.Map">
         SELECT w.*
         FROM early_warning w
         <where>
-            AND w.status IN ('PENDING', 'PROCESSING', 'RELEASED')
+            AND w.status IN ('PENDING', 'RECEIVED', 'PROCESSING', 'HANDLED')
             <if test="warningType != null and warningType != ''">
                 AND w.warning_type = #{warningType}
             </if>
@@ -954,7 +993,7 @@
         SELECT w.*
         FROM early_warning w
         <where>
-            AND w.status IN ('HANDLED', 'CLOSED')
+            AND w.status = 'CLOSED'
             <if test="warningType != null and warningType != ''">
                 AND w.warning_type = #{warningType}
             </if>
@@ -983,7 +1022,7 @@
             SUM(CASE WHEN status = 'DRAFT' THEN 1 ELSE 0 END) AS draftCount,
             SUM(CASE WHEN status = 'PENDING' THEN 1 ELSE 0 END) AS pendingCount,
             SUM(CASE WHEN status = 'PROCESSING' THEN 1 ELSE 0 END) AS processingCount,
-            SUM(CASE WHEN status = 'RELEASED' THEN 1 ELSE 0 END) AS releasedCount,
+            SUM(CASE WHEN status = 'RECEIVED' THEN 1 ELSE 0 END) AS receivedCount,
             SUM(CASE WHEN status = 'HANDLED' THEN 1 ELSE 0 END) AS handledCount,
             SUM(CASE WHEN status = 'CLOSED' THEN 1 ELSE 0 END) AS closedCount,
             SUM(CASE WHEN warning_level = '1' THEN 1 ELSE 0 END) AS level1Count,
@@ -1064,7 +1103,7 @@
         SELECT warning.industry_code,
                warning.industry_name,
                COUNT(*) AS total_count,
-               SUM(CASE WHEN warning.status IN ('PENDING', 'RELEASED') THEN 1 ELSE 0 END) AS pending_count,
+               SUM(CASE WHEN warning.status IN ('PENDING', 'RECEIVED') THEN 1 ELSE 0 END) AS pending_count,
                SUM(CASE WHEN warning.status = 'PROCESSING' THEN 1 ELSE 0 END) AS processing_count,
                SUM(CASE WHEN warning.status IN ('HANDLED', 'CLOSED', 'RESOLVED') THEN 1 ELSE 0 END) AS completed_count,
                ROUND(100.0 * SUM(CASE WHEN warning.status IN ('HANDLED', 'CLOSED', 'RESOLVED') THEN 1 ELSE 0 END)

+ 4 - 0
pipe-network-service/zksy-system/src/main/resources/mapper/warning/WarningDisposalMapper.xml

@@ -6,6 +6,10 @@
         SELECT
             d.*,
             CASE d.disposal_type
+                WHEN 'RELEASE' THEN '发布预警'
+                WHEN 'RECEIVE' THEN '接收预警'
+                WHEN 'START' THEN '开始处置'
+                WHEN 'REPORT' THEN '上传预警报告'
                 WHEN 'UPGRADE' THEN '预警升级'
                 WHEN 'DOWNGRADE' THEN '预警降级'
                 WHEN 'CONFIRM' THEN '确认异常'

+ 5 - 6
zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

@@ -30,9 +30,8 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
 
     private static final String AUTO_PREFIX = "AUTO|";
     private static final String SYSTEM_USER = "系统自动";
-    private static final List<String> OPEN_STATUS = Arrays.asList(
-            "DRAFT", "PENDING", "PROCESSING", "RELEASED"
-    );
+    // 采集侧只维护尚未发布的系统草稿。发布后进入人工流程,采集恢复/新值不能覆盖人工处置结果。
+    private static final List<String> OPEN_STATUS = Arrays.asList("DRAFT");
 
     @Autowired
     private EarlyWarningApiMapper earlyWarningApiMapper;
@@ -186,9 +185,9 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
         warning.setWarningLevel(String.valueOf(result.getLevel()));
         warning.setWarningSpecial(warningCode);
         warning.setDeviceCode(deviceCode);
-        warning.setStatus("RELEASED");
-        warning.setPublisher(SYSTEM_USER);
-        warning.setPublishTime(now);
+        // 系统自动产生的预警统一进入“草稿”初始态,由人工在预警发布页确认发布后再进入待办
+        warning.setStatus("DRAFT");
+        warning.setPublisher(null);
         warning.setCreateBy(SYSTEM_USER);
         warning.setCreateTime(now);
         warning.setUpdateTime(now);