浏览代码

Merge remote-tracking branch 'origin/master'

Kazerin 2 周之前
父节点
当前提交
5b53a6df4a
共有 38 个文件被更改,包括 866 次插入 和 616 次删除
  1. 5 340
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageEquipmentController.java
  2. 3 3
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageWarningController.java
  3. 42 17
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningController.java
  4. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningTodayController.java
  5. 5 15
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningItemController.java
  6. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningReasonController.java
  7. 1 0
      pipe-network-service/zksy-common/src/main/java/com/zksy/common/sms/SmsProperties.java
  8. 2 7
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmReviewAttachmentService.java
  9. 19 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/EarlyWarning.java
  10. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningAttachment.java
  11. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningDisposal.java
  12. 3 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningItem.java
  13. 2 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningProcessDTO.java
  14. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningSupervision.java
  15. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningTodo.java
  16. 50 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningWorkflow.java
  17. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/EarlyWarningMapper.java
  18. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/WarningItemMapper.java
  19. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/WarningReasonMapper.java
  20. 13 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/EarlyWarningService.java
  21. 53 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningCatalogRules.java
  22. 69 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningItemBindingService.java
  23. 1 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningItemService.java
  24. 45 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningSupervisionNotifier.java
  25. 331 86
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningServiceImpl.java
  26. 47 111
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningItemServiceImpl.java
  27. 15 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningReasonServiceImpl.java
  28. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/BusinessCockpitEventAdapter.java
  29. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/DrainageWarning.java
  30. 3 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageWarningService.java
  31. 2 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageWarningServiceImpl.java
  32. 2 1
      pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml
  33. 1 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/AlarmWarningRelMapper.xml
  34. 51 8
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml
  35. 4 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/WarningDisposalMapper.xml
  36. 6 0
      zk-api-service/src/main/java/com/zksy/api/domain/EarlyWarning.java
  37. 15 0
      zk-api-service/src/main/java/com/zksy/api/mapper/WarningItemBindingMapper.java
  38. 37 6
      zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

+ 5 - 340
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageEquipmentController.java

@@ -1,34 +1,26 @@
 package com.zksy.web.controller.drainage;
 
-import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.zksy.base.domain.*;
 import com.zksy.base.domain.vo.EquipmentFullVO;
 import com.zksy.base.service.EquipmentBaseService;
-import com.zksy.base.service.EquipmentMaintainService;
-import com.zksy.base.service.EquipmentStatusService;
-import com.zksy.base.service.EquipmentTypeService;
 import com.zksy.common.annotation.Anonymous;
-import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
-import com.zksy.common.enums.BusinessType;
-import com.zksy.drainage.service.DrainageEquipmentPointRelService;
 import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
-import java.util.List;
 import java.util.Map;
 
 /**
  * 排水系统-监测设备管理
  */
-@Slf4j
 @RestController
 @RequestMapping("/api/drainage/equipment")
 @Api(tags = "排水系统-监测设备管理")
@@ -37,18 +29,6 @@ public class DrainageEquipmentController {
     @Autowired
     private EquipmentBaseService equipmentBaseService;
 
-    @Autowired
-    private EquipmentTypeService equipmentTypeService;
-
-    @Autowired
-    private EquipmentStatusService equipmentStatusService;
-
-    @Autowired
-    private EquipmentMaintainService equipmentMaintainService;
-
-    @Autowired
-    private DrainageEquipmentPointRelService equipmentPointRelService;
-
     @Autowired
     private DrainageEquipmentBindingService equipmentBindingService;
 
@@ -83,319 +63,4 @@ public class DrainageEquipmentController {
         return AjaxResult.success(detail);
     }
 
-    @Anonymous
-    @GetMapping("/base/list")
-    @ApiOperation(value = "查询所有监测设备")
-    public AjaxResult baseList() {
-        return AjaxResult.success(equipmentBaseService.list());
-    }
-
-    @Anonymous
-    @PostMapping("/base")
-    @ApiOperation(value = "新增监测设备")
-    @Log(title = "新增监测设备", businessType = BusinessType.INSERT)
-    @Transactional
-    public AjaxResult baseSave(@RequestBody EquipmentBase entity) {
-        boolean saved = equipmentBaseService.saveWithCheck(entity);
-        if (saved) {
-            return AjaxResult.success("新增成功", entity);
-        }
-        return AjaxResult.error("新增失败");
-    }
-
-    @Anonymous
-    @PutMapping("/base")
-    @ApiOperation(value = "修改监测设备")
-    @Log(title = "修改监测设备", businessType = BusinessType.UPDATE)
-    @Transactional
-    public AjaxResult baseUpdate(@RequestBody EquipmentBase entity) {
-        boolean updated = equipmentBaseService.updateWithCheck(entity);
-        return updated ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/base/{id}")
-    @ApiOperation(value = "删除监测设备")
-    @Log(title = "删除监测设备", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult baseDelete(@PathVariable String id) {
-        boolean deleted = equipmentBaseService.removeWithCheck(id);
-        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/base/batch")
-    @ApiOperation(value = "批量删除监测设备")
-    @Log(title = "批量删除监测设备", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult baseDeleteBatch(@RequestBody List<String> ids) {
-        boolean deleted = equipmentBaseService.removeBatchWithCheck(ids);
-        return deleted ? AjaxResult.success("批量删除成功") : AjaxResult.error("批量删除失败");
-    }
-
-    // ==================== 设备类别 ====================
-
-    @Anonymous
-    @GetMapping("/type/page")
-    @ApiOperation(value = "设备类别分页查询")
-    public Page<EquipmentType> typePage(
-            @ApiParam("页码") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam("每页数量") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam("类别ID") @RequestParam(required = false) Integer typeId,
-            @ApiParam("类别名称") @RequestParam(required = false) String typeName,
-            @ApiParam("父类别ID") @RequestParam(required = false) Integer parentTypeId) {
-        return equipmentTypeService.findByPage(pageNum, pageSize, typeId, typeName, parentTypeId);
-    }
-
-    @Anonymous
-    @GetMapping("/type/{id}")
-    @ApiOperation(value = "查询设备类别详情")
-    public AjaxResult typeGetById(@PathVariable Integer id) {
-        EquipmentType entity = equipmentTypeService.getById(id);
-        if (entity == null) {
-            return AjaxResult.error("该设备类别不存在");
-        }
-        return AjaxResult.success(entity);
-    }
-
-    @Anonymous
-    @GetMapping("/type/list")
-    @ApiOperation(value = "查询所有设备类别")
-    public AjaxResult typeList() {
-        return AjaxResult.success(equipmentTypeService.list());
-    }
-
-    @Anonymous
-    @PostMapping("/type")
-    @ApiOperation(value = "新增设备类别")
-    @Log(title = "新增设备类别", businessType = BusinessType.INSERT)
-    @Transactional
-    public AjaxResult typeSave(@RequestBody EquipmentType entity) {
-        boolean saved = equipmentTypeService.saveWithCheck(entity);
-        return saved ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
-    }
-
-    @Anonymous
-    @PutMapping("/type")
-    @ApiOperation(value = "修改设备类别")
-    @Log(title = "修改设备类别", businessType = BusinessType.UPDATE)
-    @Transactional
-    public AjaxResult typeUpdate(@RequestBody EquipmentType entity) {
-        boolean updated = equipmentTypeService.updateWithCheck(entity);
-        return updated ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/type/{id}")
-    @ApiOperation(value = "删除设备类别")
-    @Log(title = "删除设备类别", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult typeDelete(@PathVariable Integer id) {
-        boolean deleted = equipmentTypeService.removeWithCheck(id);
-        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/type/batch")
-    @ApiOperation(value = "批量删除设备类别")
-    @Log(title = "批量删除设备类别", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult typeDeleteBatch(@RequestBody List<Integer> ids) {
-        boolean deleted = equipmentTypeService.removeBatchWithCheck(ids);
-        return deleted ? AjaxResult.success("批量删除成功") : AjaxResult.error("批量删除失败");
-    }
-
-    // ==================== 设备状态 ====================
-
-    @Anonymous
-    @GetMapping("/status/page")
-    @ApiOperation(value = "设备状态分页查询")
-    public Page<EquipmentStatus> statusPage(
-            @ApiParam("页码") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam("每页数量") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam("状态ID") @RequestParam(required = false) String statusId,
-            @ApiParam("设备ID") @RequestParam(required = false) String equipmentId,
-            @ApiParam("当前状态") @RequestParam(required = false) Integer currentStatus,
-            @ApiParam("报警状态") @RequestParam(required = false) Integer alarmStatus,
-            @ApiParam("在线状态") @RequestParam(required = false) Integer onlineStatus,
-            @ApiParam("负责人ID") @RequestParam(required = false) Long userId) {
-        return equipmentStatusService.findByPage(pageNum, pageSize, statusId, equipmentId, currentStatus, alarmStatus, onlineStatus, userId);
-    }
-
-    @Anonymous
-    @GetMapping("/status/{id}")
-    @ApiOperation(value = "查询设备状态详情")
-    public AjaxResult statusGetById(@PathVariable String id) {
-        EquipmentStatus entity = equipmentStatusService.getById(id);
-        if (entity == null) {
-            return AjaxResult.error("该设备状态记录不存在");
-        }
-        return AjaxResult.success(entity);
-    }
-
-    @Anonymous
-    @PostMapping("/status")
-    @ApiOperation(value = "新增设备状态")
-    @Log(title = "新增设备状态", businessType = BusinessType.INSERT)
-    @Transactional
-    public AjaxResult statusSave(@RequestBody EquipmentStatus entity) {
-        boolean saved = equipmentStatusService.saveWithCheck(entity);
-        return saved ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
-    }
-
-    @Anonymous
-    @PutMapping("/status")
-    @ApiOperation(value = "修改设备状态")
-    @Log(title = "修改设备状态", businessType = BusinessType.UPDATE)
-    @Transactional
-    public AjaxResult statusUpdate(@RequestBody EquipmentStatus entity) {
-        boolean updated = equipmentStatusService.updateWithCheck(entity);
-        return updated ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/status/{id}")
-    @ApiOperation(value = "删除设备状态")
-    @Log(title = "删除设备状态", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult statusDelete(@PathVariable String id) {
-        boolean deleted = equipmentStatusService.removeById(id);
-        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
-    }
-
-    // ==================== 设备维护记录 ====================
-
-    @Anonymous
-    @GetMapping("/maintain/page")
-    @ApiOperation(value = "设备维护记录分页查询")
-    public Page<EquipmentMaintain> maintainPage(
-            @ApiParam("页码") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam("每页数量") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam("维护记录ID") @RequestParam(required = false) String maintainId,
-            @ApiParam("设备ID") @RequestParam(required = false) String equipmentId,
-            @ApiParam("维护类型") @RequestParam(required = false) String maintainType,
-            @ApiParam("维护人") @RequestParam(required = false) String maintainPerson) {
-        return equipmentMaintainService.findByPage(pageNum, pageSize, maintainId, equipmentId, maintainType, maintainPerson);
-    }
-
-    @Anonymous
-    @GetMapping("/maintain/{id}")
-    @ApiOperation(value = "查询设备维护记录详情")
-    public AjaxResult maintainGetById(@PathVariable String id) {
-        EquipmentMaintain entity = equipmentMaintainService.getById(id);
-        if (entity == null) {
-            return AjaxResult.error("该维护记录不存在");
-        }
-        return AjaxResult.success(entity);
-    }
-
-    @Anonymous
-    @PostMapping("/maintain")
-    @ApiOperation(value = "新增设备维护记录")
-    @Log(title = "新增设备维护记录", businessType = BusinessType.INSERT)
-    @Transactional
-    public AjaxResult maintainSave(@RequestBody EquipmentMaintain entity) {
-        boolean saved = equipmentMaintainService.saveWithCheck(entity);
-        return saved ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
-    }
-
-    @Anonymous
-    @PutMapping("/maintain")
-    @ApiOperation(value = "修改设备维护记录")
-    @Log(title = "修改设备维护记录", businessType = BusinessType.UPDATE)
-    @Transactional
-    public AjaxResult maintainUpdate(@RequestBody EquipmentMaintain entity) {
-        boolean updated = equipmentMaintainService.updateWithCheck(entity);
-        return updated ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/maintain/{id}")
-    @ApiOperation(value = "删除设备维护记录")
-    @Log(title = "删除设备维护记录", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult maintainDelete(@PathVariable String id) {
-        boolean deleted = equipmentMaintainService.removeById(id);
-        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/maintain/batch")
-    @ApiOperation(value = "批量删除设备维护记录")
-    @Log(title = "批量删除设备维护记录", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult maintainDeleteBatch(@RequestBody List<String> ids) {
-        boolean deleted = equipmentMaintainService.removeByIds(ids);
-        return deleted ? AjaxResult.success("批量删除成功") : AjaxResult.error("批量删除失败");
-    }
-
-    // ==================== 设备-监控点关联 ====================
-
-    @Anonymous
-    @GetMapping("/pointRel/page")
-    @ApiOperation(value = "设备-监控点关联分页查询")
-    public Page<EquipmentPointRel> pointRelPage(
-            @ApiParam("页码") @RequestParam(defaultValue = "1") long pageNum,
-            @ApiParam("每页数量") @RequestParam(defaultValue = "10") long pageSize,
-            @ApiParam("设备ID") @RequestParam(required = false) String equipmentId,
-            @ApiParam("监测点ID") @RequestParam(required = false) String pointId) {
-        return equipmentPointRelService.findByPage(pageNum, pageSize, equipmentId, pointId);
-    }
-
-    @Anonymous
-    @GetMapping("/pointRel/byEquipment/{equipmentId}")
-    @ApiOperation(value = "根据设备ID查询关联监测点")
-    public AjaxResult pointRelByEquipment(@PathVariable String equipmentId) {
-        return AjaxResult.success(equipmentPointRelService.getByEquipmentId(equipmentId));
-    }
-
-    @Anonymous
-    @GetMapping("/pointRel/byPoint/{pointId}")
-    @ApiOperation(value = "根据监测点ID查询关联设备")
-    public AjaxResult pointRelByPoint(@PathVariable String pointId) {
-        return AjaxResult.success(equipmentPointRelService.getByPointId(pointId));
-    }
-
-    @Anonymous
-    @PostMapping("/pointRel")
-    @ApiOperation(value = "新增设备-监控点关联")
-    @Log(title = "新增设备-监控点关联", businessType = BusinessType.INSERT)
-    @Transactional
-    public AjaxResult pointRelSave(@RequestBody EquipmentPointRel entity) {
-        boolean saved = equipmentPointRelService.saveWithCheck(entity);
-        return saved ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
-    }
-
-    @Anonymous
-    @PostMapping("/pointRel/batch")
-    @ApiOperation(value = "批量新增设备-监控点关联")
-    @Log(title = "批量新增设备-监控点关联", businessType = BusinessType.INSERT)
-    @Transactional
-    public AjaxResult pointRelSaveBatch(@RequestBody List<EquipmentPointRel> entityList) {
-        if (CollectionUtils.isEmpty(entityList)) {
-            return AjaxResult.error("批量新增失败:关联列表不能为空");
-        }
-        boolean saved = equipmentPointRelService.saveBatchWithCheck(entityList);
-        return saved ? AjaxResult.success("批量新增成功") : AjaxResult.error("批量新增失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/pointRel/{id}")
-    @ApiOperation(value = "删除设备-监控点关联")
-    @Log(title = "删除设备-监控点关联", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult pointRelDelete(@PathVariable String id) {
-        boolean deleted = equipmentPointRelService.removeWithCheck(id);
-        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
-    }
-
-    @Anonymous
-    @DeleteMapping("/pointRel/batch")
-    @ApiOperation(value = "批量删除设备-监控点关联")
-    @Log(title = "批量删除设备-监控点关联", businessType = BusinessType.DELETE)
-    @Transactional
-    public AjaxResult pointRelDeleteBatch(@RequestBody List<String> ids) {
-        boolean deleted = equipmentPointRelService.removeBatchWithCheck(ids);
-        return deleted ? AjaxResult.success("批量删除成功") : AjaxResult.error("批量删除失败");
-    }
 }

+ 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("状态更新失败");

+ 42 - 17
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")
@@ -204,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);
@@ -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);

+ 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) {

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

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

@@ -8,7 +8,6 @@ import com.zksy.common.utils.file.FileUploadUtils;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
-import java.io.InputStream;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
@@ -23,12 +22,8 @@ public class AlarmReviewAttachmentService {
         if (file == null || file.isEmpty() || file.getSize() > MAX_BYTES)
             throw new ServiceException("附件不能为空且单个不得超过10MB");
         String name = Objects.toString(file.getOriginalFilename(), "").toLowerCase(Locale.ROOT);
-        byte[] header;
-        try (InputStream input = file.getInputStream()) { header = input.readNBytes(8); }
-        boolean jpeg = (name.endsWith(".jpg") || name.endsWith(".jpeg")) && header.length >= 3
-                && (header[0] & 255) == 255 && (header[1] & 255) == 216 && (header[2] & 255) == 255;
-        boolean png = name.endsWith(".png") && Arrays.equals(header, new byte[]{(byte)137,80,78,71,13,10,26,10});
-        if (!jpeg && !png) throw new ServiceException("仅支持内容与扩展名一致的JPG、JPEG、PNG图片");
+        if (!(name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png")))
+            throw new ServiceException("仅支持JPG、JPEG、PNG后缀的附件");
         String path = FileUploadUtils.upload(ZksyConfig.getUploadPath(), file, new String[]{"jpg", "jpeg", "png"}, true);
         String token = UUID.randomUUID().toString();
         redis.setCacheObject(key(alarmId, token), path, 2, TimeUnit.HOURS);

+ 19 - 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("关联设备编码")
@@ -82,6 +97,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;

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

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

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

+ 13 - 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,16 +44,22 @@ 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);
 
+    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);
@@ -58,7 +68,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);
 

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

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

+ 331 - 86
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,11 +31,14 @@ import java.time.format.DateTimeFormatter;
 import java.util.*;
 
 @Service
+@Slf4j
 public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, EarlyWarning>
         implements EarlyWarningService {
 
     @Autowired
     private EarlyWarningMapper earlyWarningMapper;
+    @Autowired private com.zksy.base.warning.service.WarningItemBindingService itemBinding;
+    @Autowired private WarningReasonMapper warningReasonMapper;
     @Autowired
     private AlarmWarningRelMapper alarmWarningRelMapper;
     @Autowired
@@ -42,6 +55,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 +76,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 +115,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,12 +188,23 @@ 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());
         warning.setUpdateTime(LocalDateTime.now());
 
+        itemBinding.bind(warning);
+        warning.setDisposalDeadline(null);
+        warning.setStageDeadline(null);
         boolean result = save(warning);
         if (result) {
             if (alarmIds != null && !alarmIds.isEmpty()) {
@@ -174,11 +224,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 +248,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 +269,19 @@ 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);
+        itemBinding.bind(warning);
+        warning.setDisposalDeadline(null);
+        warning.setStageDeadline(null);
+        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 +298,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,23 +359,39 @@ 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);
+
+        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);
 
         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,47 +400,65 @@ 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");
+        itemBinding.startStage(warning, "HANDLE", now);
         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;
+        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);
+        if (isBlank(processContent)) throw new ServiceException("请填写现场处置结果");
         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;
@@ -353,23 +467,48 @@ 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()) {
             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");
+        warning.setStageDeadline(null);
         warning.setRemark(clearRemark);
         warning.setUpdateTime(now);
         if (!updateById(warning)) return false;
@@ -385,23 +524,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 +605,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 +613,113 @@ 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");
+        itemBinding.startStage(warning, "HANDLE", LocalDateTime.now());
+        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 +728,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 +905,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 +925,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 +991,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 +1068,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 +1124,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;
     }

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

+ 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) {

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

+ 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

+ 51 - 8
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"/>
@@ -18,6 +22,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"/>
@@ -30,8 +35,8 @@
 
     <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,
+        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
     </sql>
@@ -784,7 +789,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 +832,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 +940,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 +997,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 +1026,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 +1107,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 '确认异常'

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

+ 37 - 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;
@@ -40,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,
@@ -125,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);
@@ -165,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={}%",
@@ -186,9 +190,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);
@@ -211,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>()