1
0

3 Коммиты 7ae0571f83 ... c3507af38a

Автор SHA1 Сообщение Дата
  林仔 c3507af38a feat(warning): 优化预警系统功能并增强权限控制 1 неделя назад
  林仔 ad59f085df Merge remote-tracking branch 'origin/master' 1 неделя назад
  林仔 f64a473661 feat(warning): 添加预警仪表盘功能 2 недель назад
24 измененных файлов с 1282 добавлено и 110 удалено
  1. 13 4
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningDisposeEfficiencyController.java
  2. 18 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningDisposeStatusController.java
  3. 11 3
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningReceiveEfficiencyController.java
  4. 4 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningTodayController.java
  5. 4 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningTrendController.java
  6. 94 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningDashboardController.java
  7. 13 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningItemController.java
  8. 2 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningReasonController.java
  9. 2 2
      pipe-network-service/zksy-admin/src/main/resources/mybatis/mybatis-config.xml
  10. 82 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/EarlyWarningMapper.java
  11. 5 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/IEarlyWarningDisposeEfficiencyService.java
  12. 5 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/IEarlyWarningDisposeStatusService.java
  13. 3 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/IEarlyWarningReceiveEfficiencyService.java
  14. 82 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/WarningDashboardService.java
  15. 10 7
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningDisposeEfficiencyServiceImpl.java
  16. 8 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningDisposeStatusServiceImpl.java
  17. 7 8
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningReceiveEfficiencyServiceImpl.java
  18. 2 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningTodayServiceImpl.java
  19. 305 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningDashboardServiceImpl.java
  20. 64 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningItemServiceImpl.java
  21. 72 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/WarningReasonServiceImpl.java
  22. 8 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/impl/SysMenuServiceImpl.java
  23. 466 47
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml
  24. 2 0
      zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

+ 13 - 4
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningDisposeEfficiencyController.java

@@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.security.access.prepost.PreAuthorize;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -23,7 +24,7 @@ import java.util.Map;
  * 预警统计-处置时效分析接口
  */
 @RestController
-@RequestMapping("/api/warning/disposeEfficiency")
+@RequestMapping("/warning/disposeEfficiency")
 @Api(tags = "预警信息-处置时效分析模块")
 public class EarlyWarningDisposeEfficiencyController {
 
@@ -33,6 +34,7 @@ public class EarlyWarningDisposeEfficiencyController {
     private IEarlyWarningDisposeEfficiencyService efficiencyService;
 
     @GetMapping("/overview")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:efficiency')")
     @ApiOperation(value = "处置时效概览统计", notes = "返回平均、最长、最短处置时长及总数量核心指标")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -59,6 +61,7 @@ public class EarlyWarningDisposeEfficiencyController {
     }
 
     @GetMapping("/range")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:efficiency')")
     @ApiOperation(value = "处置时效区间分布", notes = "按时长区间统计预警数量,适配柱状图展示")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -85,6 +88,7 @@ public class EarlyWarningDisposeEfficiencyController {
     }
 
     @GetMapping("/special")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:efficiency')")
     @ApiOperation(value = "按专项统计处置时效", notes = "按预警专项维度统计平均处置时长,从高到低排序")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -97,9 +101,13 @@ public class EarlyWarningDisposeEfficiencyController {
 
             @ApiParam(value = "发布结束时间 yyyy-MM-dd HH:mm:ss", example = "2026-06-01 23:59:59")
             @RequestParam(required = false)
-            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) {
+            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+
+            @ApiParam(value = "预警专项", example = "管网压力专项")
+            @RequestParam(required = false) String warningSpecial) {
         try {
-            List<Map<String, Object>> result = efficiencyService.getEfficiencyBySpecial(startTime, endTime);
+            List<Map<String, Object>> result = efficiencyService.getEfficiencyBySpecial(
+                    startTime, endTime, warningSpecial);
             return AjaxResult.success("专项时效统计查询成功", result);
         } catch (Exception e) {
             log.error("专项处置时效统计异常", e);
@@ -108,6 +116,7 @@ public class EarlyWarningDisposeEfficiencyController {
     }
 
     @GetMapping("/page")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:efficiency')")
     @ApiOperation(value = "处置时效明细列表", notes = "分页查询预警处置时效明细,支持按时长排序,定位低效案例")
     @ApiResponses({
             @ApiResponse(code = 200, message = "查询成功", response = AjaxResult.class),
@@ -139,4 +148,4 @@ public class EarlyWarningDisposeEfficiencyController {
             return AjaxResult.error("查询失败:" + e.getMessage());
         }
     }
-}
+}

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

@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.access.prepost.PreAuthorize;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -34,6 +35,7 @@ public class EarlyWarningDisposeStatusController {
     private IEarlyWarningDisposeStatusService disposeStatusService;
 
     @GetMapping("/stat")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:status')")
     @ApiOperation(value = "处置状态统计分析",
             notes = "按预警处置状态分组统计数量,支持时间范围、预警专项筛选,返回数据适配饼图/柱状图")
     @ApiResponses({
@@ -59,4 +61,19 @@ public class EarlyWarningDisposeStatusController {
             return AjaxResult.error("统计失败:" + e.getMessage());
         }
     }
-}
+
+    @GetMapping("/disposal-type")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:status')")
+    @ApiOperation("按处置类型统计预警数量")
+    public AjaxResult getDisposalTypeStat(
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @RequestParam(required = false) String warningSpecial) {
+        try {
+            return AjaxResult.success(disposeStatusService.getDisposalTypeStat(startTime, endTime, warningSpecial));
+        } catch (Exception e) {
+            log.error("按处置类型统计异常", e);
+            return AjaxResult.error("统计失败:" + e.getMessage());
+        }
+    }
+}

+ 11 - 3
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningReceiveEfficiencyController.java

@@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.security.access.prepost.PreAuthorize;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -33,6 +34,7 @@ public class EarlyWarningReceiveEfficiencyController {
     private IEarlyWarningReceiveEfficiencyService receiveEfficiencyService;
 
     @GetMapping("/overview")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:receive')")
     @ApiOperation(value = "接警效率概览统计", notes = "返回平均、最长、最短接警时长及接警总数核心指标")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -66,6 +68,7 @@ public class EarlyWarningReceiveEfficiencyController {
     }
 
     @GetMapping("/range")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:receive')")
     @ApiOperation(value = "接警时效区间分布", notes = "按响应时长区间统计预警数量,适配柱状图展示")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -99,6 +102,7 @@ public class EarlyWarningReceiveEfficiencyController {
     }
 
     @GetMapping("/type")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:receive')")
     @ApiOperation(value = "按类型接警统计接警效率", notes = "按预警类型维度统计平均接警时长,从高到低排序")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -111,9 +115,12 @@ public class EarlyWarningReceiveEfficiencyController {
 
             @ApiParam(value = "发布结束时间 yyyy-MM-dd HH:mm:ss", example = "2026-06-01 23:59:59")
             @RequestParam(required = false)
-            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) {
+            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @RequestParam(required = false) String warningType,
+            @RequestParam(required = false) String warningSpecial,
+            @RequestParam(required = false) String status) {
         try {
-            List<Map<String, Object>> result = receiveEfficiencyService.getEfficiencyByType(startTime, endTime);
+            List<Map<String, Object>> result = receiveEfficiencyService.getEfficiencyByType(startTime, endTime, warningType, warningSpecial, status);
             return AjaxResult.success("类型维度统计查询成功", result);
         } catch (Exception e) {
             log.error("按类型统计接警效率异常", e);
@@ -122,6 +129,7 @@ public class EarlyWarningReceiveEfficiencyController {
     }
 
     @GetMapping("/page")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:receive')")
     @ApiOperation(value = "接警时效明细列表", notes = "分页查询预警接警时效明细,支持按时长排序,定位低效响应案例")
     @ApiResponses({
             @ApiResponse(code = 200, message = "查询成功", response = AjaxResult.class),
@@ -160,4 +168,4 @@ public class EarlyWarningReceiveEfficiencyController {
             return AjaxResult.error("查询失败:" + e.getMessage());
         }
     }
-}
+}

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

@@ -14,6 +14,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.security.access.prepost.PreAuthorize;
 
 import java.util.Map;
 
@@ -31,6 +32,7 @@ public class EarlyWarningTodayController {
     private IEarlyWarningTodayService earlyWarningTodayService;
 
     @GetMapping("/stats")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:today')")
     @ApiOperation(value = "获取今日预警统计指标", notes = "返回今日预警总数、已处置数、已解除数三个核心指标")
     @ApiResponses({
             @ApiResponse(code = 200, message = "查询成功", response = AjaxResult.class),
@@ -47,6 +49,7 @@ public class EarlyWarningTodayController {
     }
 
     @GetMapping("/page")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:today')")
     @ApiOperation(value = "分页查询今日预警列表", notes = "支持按状态筛选,为空则查询所有非草稿状态的今日预警")
     @ApiResponses({
             @ApiResponse(code = 200, message = "查询成功", response = AjaxResult.class),
@@ -65,4 +68,4 @@ public class EarlyWarningTodayController {
             return AjaxResult.error("查询失败:" + e.getMessage());
         }
     }
-}
+}

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

@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.security.access.prepost.PreAuthorize;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -31,6 +32,7 @@ public class EarlyWarningTrendController {
     private IEarlyWarningTrendService earlyWarningTrendService;
 
     @GetMapping("/day")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:trend')")
     @ApiOperation(value = "按日统计预警发展趋势", notes = "按日期维度统计各专项预警数量趋势,支持时间范围、预警专项筛选")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -55,6 +57,7 @@ public class EarlyWarningTrendController {
     }
 
     @GetMapping("/month")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:trend')")
     @ApiOperation(value = "按月统计预警发展趋势", notes = "按月份维度统计各专项预警数量趋势,支持时间范围、预警专项筛选")
     @ApiResponses({
             @ApiResponse(code = 200, message = "统计成功", response = AjaxResult.class),
@@ -77,4 +80,4 @@ public class EarlyWarningTrendController {
             return AjaxResult.error("统计失败:" + e.getMessage());
         }
     }
-}
+}

+ 94 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/WarningDashboardController.java

@@ -0,0 +1,94 @@
+package com.zksy.web.controller.warning;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.warning.service.WarningDashboardService;
+import com.zksy.common.core.domain.AjaxResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * Warning overview APIs for the three dynamic-menu pages.
+ */
+@RestController
+@RequestMapping("/api/warning-dashboard")
+@Api(tags = "预警总览")
+public class WarningDashboardController {
+
+    private static final int MAX_PAGE_SIZE = 100;
+
+    @Autowired
+    private WarningDashboardService warningDashboardService;
+
+    @GetMapping("/today-overview")
+    @ApiOperation("今日预警概况")
+    @PreAuthorize("@ss.hasAnyPermi('warning:overview:today,warning:warning:query')")
+    public AjaxResult getTodayOverview() {
+        return AjaxResult.success(warningDashboardService.getTodayOverview());
+    }
+
+    @GetMapping("/today-overview/page")
+    @ApiOperation("分页查询未解除、今日已解除或今日全部预警")
+    @PreAuthorize("@ss.hasAnyPermi('warning:overview:today,warning:warning:query')")
+    public AjaxResult getTodayOverviewPage(
+            @ApiParam("unresolved-未解除,resolved-今日已解除,all-今日全部")
+            @RequestParam(defaultValue = "unresolved") String category,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String warningType,
+            @RequestParam(required = false) String warningLevel,
+            @RequestParam(required = false) String keyword) {
+        Page<Map<String, Object>> page = createPage(pageNum, pageSize);
+        return AjaxResult.success(warningDashboardService.getTodayOverviewPage(
+                page, category, warningType, warningLevel, keyword));
+    }
+
+    @GetMapping("/history-overview")
+    @ApiOperation("历史预警概况")
+    @PreAuthorize("@ss.hasAnyPermi('warning:overview:history,warning:warning:query')")
+    public AjaxResult getHistoryOverview() {
+        return AjaxResult.success(warningDashboardService.getHistoryOverview());
+    }
+
+    @GetMapping("/history-overview/page")
+    @ApiOperation("分页查询历史预警明细")
+    @PreAuthorize("@ss.hasAnyPermi('warning:overview:history,warning:warning:query')")
+    public AjaxResult getHistoryOverviewPage(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String warningType,
+            @RequestParam(required = false) String warningSpecial,
+            @RequestParam(required = false) String keyword) {
+        Page<Map<String, Object>> page = createPage(pageNum, pageSize);
+        return AjaxResult.success(warningDashboardService.getHistoryWarningPage(
+                page, warningType, warningSpecial, keyword));
+    }
+
+    @GetMapping("/today-unprocessed")
+    @ApiOperation("今日未处置预警(截至当前未处置积压)")
+    @PreAuthorize("@ss.hasAnyPermi('warning:overview:unprocessed,warning:warning:query')")
+    public AjaxResult getTodayUnprocessed(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String warningType,
+            @RequestParam(required = false) String warningLevel,
+            @RequestParam(required = false) String keyword) {
+        Page<Map<String, Object>> page = createPage(pageNum, pageSize);
+        return AjaxResult.success(warningDashboardService.getTodayUnprocessed(
+                page, warningType, warningLevel, keyword));
+    }
+
+    private Page<Map<String, Object>> createPage(Integer pageNum, Integer pageSize) {
+        int safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
+        int safePageSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, MAX_PAGE_SIZE);
+        return new Page<>(safePageNum, safePageSize);
+    }
+}

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

@@ -80,7 +80,7 @@ public class WarningItemController {
 
     @GetMapping("/detail/{itemId}")
     @ApiOperation("预警事项详情-获取事项详细信息")
-    @PreAuthorize("@ss.hasPermi('warning:item:query')")
+    @PreAuthorize("@ss.hasAnyPermi('warning:item:list,warning:item:query,warning:item:edit')")
     public AjaxResult getItemDetail(@PathVariable String itemId) {
         WarningItem item = warningItemService.getItemDetail(itemId);
         if (item != null) {
@@ -90,9 +90,20 @@ public class WarningItemController {
         }
     }
 
+    @GetMapping("/detail/code/{itemCode}")
+    @ApiOperation("预警事项详情-根据事项编码获取详细信息")
+    @PreAuthorize("@ss.hasAnyPermi('warning:item:list,warning:item:query,warning:item:edit')")
+    public AjaxResult getItemDetailByCode(@PathVariable String itemCode) {
+        WarningItem item = warningItemService.getItemByCode(itemCode);
+        if (item != null) {
+            return AjaxResult.success(item);
+        }
+        return AjaxResult.error("未查询到事项编码为 " + itemCode + " 的预警事项");
+    }
+
     @GetMapping("/code/generate")
     @ApiOperation("事项编码生成-自动生成事项编码")
-    @PreAuthorize("@ss.hasPermi('warning:item:query')")
+    @PreAuthorize("@ss.hasAnyPermi('warning:item:query,warning:item:add,warning:item:edit')")
     public AjaxResult generateItemCode(
             @ApiParam("预警类型") @RequestParam String warningType,
             @ApiParam("预警级别") @RequestParam String warningLevel) {

+ 2 - 2
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.hasPermi('warning:reason:query')")
+    @PreAuthorize("@ss.hasAnyPermi('warning:reason:query,warning:item:query')")
     public AjaxResult getReasonsByTypeAndLevel(
             @ApiParam("预警类型") @RequestParam String warningType,
             @ApiParam("预警级别") @RequestParam String warningLevel) {
@@ -91,7 +91,7 @@ public class WarningReasonController {
 
     @GetMapping("/detail/{reasonId}")
     @ApiOperation("预警原因库管理-获取原因详细信息")
-    @PreAuthorize("@ss.hasPermi('warning:reason:query')")
+    @PreAuthorize("@ss.hasAnyPermi('warning:reason:list,warning:reason:query,warning:reason:edit')")
     public AjaxResult getReasonDetail(@PathVariable String reasonId) {
         WarningReason reason = warningReasonService.getReasonDetail(reasonId);
         if (reason != null) {

+ 2 - 2
pipe-network-service/zksy-admin/src/main/resources/mybatis/mybatis-config.xml

@@ -7,8 +7,8 @@ PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
     <settings>
         <!-- 使全局的映射器启用或禁用缓存 -->
         <setting name="cacheEnabled"             value="true"   />
-        <!-- 允许JDBC 支持自动生成主键 -->
-        <setting name="useGeneratedKeys"         value="true"   />
+        <!-- 允许JDBC 支持自动生成主键(Kingbase不支持,已关闭) -->
+        <setting name="useGeneratedKeys"         value="false"   />
         <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
         <setting name="defaultExecutorType"      value="SIMPLE" />
 		<!-- 指定 MyBatis 所用日志的具体实现 -->

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

@@ -38,7 +38,9 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
      * 查询今日预警列表(支持按状态筛选)
      * @param status 预警状态,为空则查询所有非草稿状态
      */
-    List<EarlyWarning> selectTodayWarningList(@Param("status") String status);
+    IPage<EarlyWarning> selectTodayWarningList(
+            Page<EarlyWarning> page,
+            @Param("status") String status);
 
     /**
      * 按日统计预警发展趋势(按专项分组)
@@ -74,6 +76,11 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
             @Param("endTime") LocalDateTime endTime,
             @Param("warningSpecial") String warningSpecial);
 
+    List<Map<String, Object>> selectDisposalGroupByType(
+            @Param("startTime") LocalDateTime startTime,
+            @Param("endTime") LocalDateTime endTime,
+            @Param("warningSpecial") String warningSpecial);
+
     /**
      * 处置时效分析-概览指标统计
      * @param startTime 发布开始时间
@@ -102,11 +109,13 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
      * 处置时效分析-按预警专项统计平均时效
      * @param startTime 发布开始时间
      * @param endTime 发布结束时间
+     * @param warningSpecial 预警专项(可选)
      * @return 各专项平均处置时长
      */
     List<Map<String, Object>> selectDisposeEfficiencyBySpecial(
             @Param("startTime") LocalDateTime startTime,
-            @Param("endTime") LocalDateTime endTime);
+            @Param("endTime") LocalDateTime endTime,
+            @Param("warningSpecial") String warningSpecial);
 
     /**
      * 处置时效分析-明细列表
@@ -116,7 +125,8 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
      * @param sortType 排序方式 asc-时效从短到长 desc-时效从长到短
      * @return 预警时效明细
      */
-    List<Map<String, Object>> selectDisposeEfficiencyList(
+    IPage<Map<String, Object>> selectDisposeEfficiencyList(
+            Page<Map<String, Object>> page,
             @Param("startTime") LocalDateTime startTime,
             @Param("endTime") LocalDateTime endTime,
             @Param("warningSpecial") String warningSpecial,
@@ -162,7 +172,10 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
      */
     List<Map<String, Object>> selectReceiveEfficiencyByType(
             @Param("startTime") LocalDateTime startTime,
-            @Param("endTime") LocalDateTime endTime);
+            @Param("endTime") LocalDateTime endTime,
+            @Param("warningType") String warningType,
+            @Param("warningSpecial") String warningSpecial,
+            @Param("status") String status);
 
     /**
      * 接警效率分析-明细列表
@@ -174,11 +187,75 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
      * @param sortType 排序方式 asc-时效从短到长 desc-时效从长到短
      * @return 预警接警时效明细
      */
-    List<Map<String, Object>> selectReceiveEfficiencyList(
+    IPage<Map<String, Object>> selectReceiveEfficiencyList(
+            Page<Map<String, Object>> page,
             @Param("startTime") LocalDateTime startTime,
             @Param("endTime") LocalDateTime endTime,
             @Param("warningType") String warningType,
             @Param("warningSpecial") String warningSpecial,
             @Param("status") String status,
             @Param("sortType") String sortType);
+
+    /**
+     * Warning overview: current active warnings, warnings resolved today and
+     * warnings published today. Drafts and unsupported facility types are excluded.
+     */
+    Map<String, Object> selectDashboardTodaySummary();
+
+    /**
+     * Warning overview preview list. category is controlled by the service and can
+     * only be unresolved or resolved.
+     */
+    List<Map<String, Object>> selectDashboardTodayPreview(
+            @Param("category") String category,
+            @Param("limit") Integer limit);
+
+    /**
+     * Warning overview paged details for the active or resolved-today category.
+     */
+    IPage<Map<String, Object>> selectDashboardTodayPage(
+            Page<Map<String, Object>> page,
+            @Param("category") String category,
+            @Param("warningType") String warningType,
+            @Param("warningLevel") String warningLevel,
+            @Param("keyword") String keyword);
+
+    /**
+     * Historical warning headline indicators.
+     */
+    Map<String, Object> selectDashboardHistorySummary();
+
+    /**
+     * Historical warning distribution grouped by warning special.
+     */
+    List<Map<String, Object>> selectDashboardHistorySpecialStats(
+            @Param("warningType") String warningType);
+
+    /**
+     * Historical warning detail page.
+     */
+    IPage<Map<String, Object>> selectDashboardHistoryPage(
+            Page<Map<String, Object>> page,
+            @Param("warningType") String warningType,
+            @Param("warningSpecial") String warningSpecial,
+            @Param("keyword") String keyword);
+
+    /**
+     * Current warnings which have not completed disposal.
+     */
+    IPage<Map<String, Object>> selectDashboardUnprocessedPage(
+            Page<Map<String, Object>> page,
+            @Param("warningType") String warningType,
+            @Param("warningLevel") String warningLevel,
+            @Param("keyword") String keyword);
+
+    /**
+     * Headline metrics for the current unprocessed-warning filter.  Keeping
+     * these aggregates in SQL prevents the dashboard cards from reflecting
+     * only the currently visible page.
+     */
+    Map<String, Object> selectDashboardUnprocessedSummary(
+            @Param("warningType") String warningType,
+            @Param("warningLevel") String warningLevel,
+            @Param("keyword") String keyword);
 }

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

@@ -22,7 +22,10 @@ public interface IEarlyWarningDisposeEfficiencyService {
     /**
      * 按预警专项统计平均处置时效
      */
-    List<Map<String, Object>> getEfficiencyBySpecial(LocalDateTime startTime, LocalDateTime endTime);
+    List<Map<String, Object>> getEfficiencyBySpecial(
+            LocalDateTime startTime,
+            LocalDateTime endTime,
+            String warningSpecial);
 
     /**
      * 分页查询处置时效明细列表
@@ -32,4 +35,4 @@ public interface IEarlyWarningDisposeEfficiencyService {
                                                    LocalDateTime endTime,
                                                    String warningSpecial,
                                                    String sortType);
-}
+}

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

@@ -16,4 +16,8 @@ public interface IEarlyWarningDisposeStatusService {
     List<Map<String, Object>> getWarningStatusStat(LocalDateTime startTime,
                                                    LocalDateTime endTime,
                                                    String warningSpecial);
-}
+
+    List<Map<String, Object>> getDisposalTypeStat(LocalDateTime startTime,
+                                                  LocalDateTime endTime,
+                                                  String warningSpecial);
+}

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

@@ -30,7 +30,8 @@ public interface IEarlyWarningReceiveEfficiencyService {
     /**
      * 按预警类型统计平均接警时效
      */
-    List<Map<String, Object>> getEfficiencyByType(LocalDateTime startTime, LocalDateTime endTime);
+    List<Map<String, Object>> getEfficiencyByType(LocalDateTime startTime, LocalDateTime endTime,
+                                                  String warningType, String warningSpecial, String status);
 
     /**
      * 分页查询接警时效明细列表
@@ -42,4 +43,4 @@ public interface IEarlyWarningReceiveEfficiencyService {
                                                    String warningSpecial,
                                                    String status,
                                                    String sortType);
-}
+}

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

@@ -0,0 +1,82 @@
+package com.zksy.base.warning.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+import java.util.Map;
+
+/**
+ * 预警仪表盘服务接口
+ * <p>
+ * 提供预警大屏可视化所需的统计数据,包括:
+ * <ul>
+ *   <li>今日预警概况 — 截至当前仍未解除 / 今日已解除的预警统计</li>
+ *   <li>历史预警概况 — 各专项预警累计数量和分布</li>
+ *   <li>今日未处置预警 — 截至当前未处置积压的预警列表</li>
+ * </ul>
+ *
+ * @author zksy
+ */
+public interface WarningDashboardService {
+
+    /**
+     * 今日预警概况
+     * <p>
+     * 以当前日期为维度,统计截止目前仍未解除的预警数量和今日已解除的预警数量,
+     * 并返回预警详情描述信息。
+     *
+     * @return { unresolvedCount, resolvedCount, unresolvedList, resolvedList }
+     */
+    Map<String, Object> getTodayOverview();
+
+    /**
+     * Query active or resolved-today warning details with database pagination.
+     */
+    IPage<Map<String, Object>> getTodayOverviewPage(
+            Page<Map<String, Object>> page,
+            String category,
+            String warningType,
+            String warningLevel,
+            String keyword);
+
+    /**
+     * 历史预警概况
+     * <p>
+     * 以城市设施运行的历史预警事件累计维度,展示各专项预警总体数量和分布情况,
+     * 支持对具体预警事件进行详情查看。
+     *
+     * @return { totalCount, specialStats: [{specialName, count, percentage}], detailList }
+     */
+    Map<String, Object> getHistoryOverview();
+
+    /**
+     * Query historical warning details with database pagination.
+     */
+    IPage<Map<String, Object>> getHistoryWarningPage(
+            Page<Map<String, Object>> page,
+            String warningType,
+            String warningSpecial,
+            String keyword);
+
+    /**
+     * 今日未处置预警
+     * <p>
+     * 针对截至当前仍未及时处置的预警事件进行集中展示,让监管者重点关注。
+     *
+     * @return { total, list: [{warningId, warningName, warningType, warningLevel, warningSpecial,
+     *          location, ownershipUnit, publisher, publishTime, status, warningContent,
+     *          todoInfo: {userName, taskName, createTime}}] }
+     */
+    Map<String, Object> getTodayUnprocessed(
+            Page<Map<String, Object>> page,
+            String warningType,
+            String warningLevel,
+            String keyword);
+
+    /**
+     * Backward-compatible unpaged form for existing callers.
+     */
+    default Map<String, Object> getTodayUnprocessed() {
+        return getTodayUnprocessed(new Page<>(1, 100), null, null, null);
+    }
+}

+ 10 - 7
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningDisposeEfficiencyServiceImpl.java

@@ -28,8 +28,12 @@ public class EarlyWarningDisposeEfficiencyServiceImpl implements IEarlyWarningDi
     }
 
     @Override
-    public List<Map<String, Object>> getEfficiencyBySpecial(LocalDateTime startTime, LocalDateTime endTime) {
-        return earlyWarningMapper.selectDisposeEfficiencyBySpecial(startTime, endTime);
+    public List<Map<String, Object>> getEfficiencyBySpecial(
+            LocalDateTime startTime,
+            LocalDateTime endTime,
+            String warningSpecial) {
+        return earlyWarningMapper.selectDisposeEfficiencyBySpecial(
+                startTime, endTime, warningSpecial);
     }
 
     @Override
@@ -38,9 +42,8 @@ public class EarlyWarningDisposeEfficiencyServiceImpl implements IEarlyWarningDi
                                                           LocalDateTime endTime,
                                                           String warningSpecial,
                                                           String sortType) {
-        List<Map<String, Object>> list = earlyWarningMapper.selectDisposeEfficiencyList(startTime, endTime, warningSpecial, sortType);
-        page.setRecords(list);
-        page.setTotal(list.size());
-        return page;
+        String safeSortType = "asc".equalsIgnoreCase(sortType) ? "asc" : "desc";
+        return earlyWarningMapper.selectDisposeEfficiencyList(
+                page, startTime, endTime, warningSpecial, safeSortType);
     }
-}
+}

+ 8 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningDisposeStatusServiceImpl.java

@@ -21,4 +21,11 @@ public class EarlyWarningDisposeStatusServiceImpl implements IEarlyWarningDispos
                                                           String warningSpecial) {
         return earlyWarningMapper.selectWarningGroupByStatus(startTime, endTime, warningSpecial);
     }
-}
+
+    @Override
+    public List<Map<String, Object>> getDisposalTypeStat(LocalDateTime startTime,
+                                                         LocalDateTime endTime,
+                                                         String warningSpecial) {
+        return earlyWarningMapper.selectDisposalGroupByType(startTime, endTime, warningSpecial);
+    }
+}

+ 7 - 8
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningReceiveEfficiencyServiceImpl.java

@@ -36,8 +36,9 @@ public class EarlyWarningReceiveEfficiencyServiceImpl implements IEarlyWarningRe
     }
 
     @Override
-    public List<Map<String, Object>> getEfficiencyByType(LocalDateTime startTime, LocalDateTime endTime) {
-        return earlyWarningMapper.selectReceiveEfficiencyByType(startTime, endTime);
+    public List<Map<String, Object>> getEfficiencyByType(LocalDateTime startTime, LocalDateTime endTime,
+                                                         String warningType, String warningSpecial, String status) {
+        return earlyWarningMapper.selectReceiveEfficiencyByType(startTime, endTime, warningType, warningSpecial, status);
     }
 
     @Override
@@ -48,10 +49,8 @@ public class EarlyWarningReceiveEfficiencyServiceImpl implements IEarlyWarningRe
                                                           String warningSpecial,
                                                           String status,
                                                           String sortType) {
-        List<Map<String, Object>> list = earlyWarningMapper.selectReceiveEfficiencyList(
-                startTime, endTime, warningType, warningSpecial, status, sortType);
-        page.setRecords(list);
-        page.setTotal(list.size());
-        return page;
+        String safeSortType = "asc".equalsIgnoreCase(sortType) ? "asc" : "desc";
+        return earlyWarningMapper.selectReceiveEfficiencyList(
+                page, startTime, endTime, warningType, warningSpecial, status, safeSortType);
     }
-}
+}

+ 2 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningTodayServiceImpl.java

@@ -8,7 +8,6 @@ import com.zksy.base.warning.service.IEarlyWarningTodayService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.List;
 import java.util.Map;
 
 @Service
@@ -24,9 +23,6 @@ public class EarlyWarningTodayServiceImpl implements IEarlyWarningTodayService {
 
     @Override
     public IPage<EarlyWarning> queryTodayWarningPage(Page<EarlyWarning> page, String status) {
-        List<EarlyWarning> list = earlyWarningMapper.selectTodayWarningList(status);
-        page.setRecords(list);
-        page.setTotal(list.size());
-        return page;
+        return earlyWarningMapper.selectTodayWarningList(page, status);
     }
-}
+}

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

@@ -0,0 +1,305 @@
+package com.zksy.base.warning.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.warning.mapper.EarlyWarningMapper;
+import com.zksy.base.warning.service.WarningDashboardService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Database-backed warning overview service.
+ *
+ * The overview deliberately excludes drafts and warning types outside the four
+ * city-life facilities. Aggregation and pagination stay in SQL so the dashboard
+ * does not load the complete warning table into application memory.
+ */
+@Slf4j
+@Service
+public class WarningDashboardServiceImpl implements WarningDashboardService {
+
+    static final String CATEGORY_UNRESOLVED = "unresolved";
+    static final String CATEGORY_RESOLVED = "resolved";
+    static final String CATEGORY_ALL = "all";
+    private static final int PREVIEW_LIMIT = 8;
+    private static final DateTimeFormatter DATE_TIME_FORMATTER =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Autowired
+    private EarlyWarningMapper earlyWarningMapper;
+
+    @Override
+    public Map<String, Object> getTodayOverview() {
+        Map<String, Object> summary = nullToEmpty(earlyWarningMapper.selectDashboardTodaySummary());
+        List<Map<String, Object>> unresolved = normalizeRecords(
+                earlyWarningMapper.selectDashboardTodayPreview(CATEGORY_UNRESOLVED, PREVIEW_LIMIT));
+        List<Map<String, Object>> resolved = normalizeRecords(
+                earlyWarningMapper.selectDashboardTodayPreview(CATEGORY_RESOLVED, PREVIEW_LIMIT));
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("todayTotal", numberValue(summary, "today_total", "todayTotal"));
+        result.put("unresolvedCount", numberValue(summary, "unresolved_count", "unresolvedCount"));
+        result.put("resolvedCount", numberValue(summary, "resolved_count", "resolvedCount"));
+        result.put("unresolvedList", unresolved);
+        result.put("resolvedList", resolved);
+        result.put("previewLimit", PREVIEW_LIMIT);
+        result.put("generatedAt", LocalDateTime.now().format(DATE_TIME_FORMATTER));
+        return result;
+    }
+
+    @Override
+    public IPage<Map<String, Object>> getTodayOverviewPage(
+            Page<Map<String, Object>> page,
+            String category,
+            String warningType,
+            String warningLevel,
+            String keyword) {
+        String safeCategory = normalizeCategory(category);
+        IPage<Map<String, Object>> result = earlyWarningMapper.selectDashboardTodayPage(
+                page,
+                safeCategory,
+                trimToNull(warningType),
+                trimToNull(warningLevel),
+                trimToNull(keyword));
+        if (result == null) {
+            result = page;
+            result.setTotal(0);
+        }
+        result.setRecords(normalizeRecords(result.getRecords()));
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> getHistoryOverview() {
+        Map<String, Object> summary = nullToEmpty(earlyWarningMapper.selectDashboardHistorySummary());
+        List<Map<String, Object>> rawSpecialStats =
+                earlyWarningMapper.selectDashboardHistorySpecialStats(null);
+        List<Map<String, Object>> specialStats = new ArrayList<>();
+        if (rawSpecialStats != null) {
+            for (Map<String, Object> row : rawSpecialStats) {
+                Map<String, Object> item = new LinkedHashMap<>();
+                item.put("specialName", stringValue(row, "special_name", "specialName"));
+                item.put("count", numberValue(row, "warning_count", "warningCount", "count"));
+                item.put("percentage", decimalValue(row, "percentage"));
+                specialStats.add(item);
+            }
+        }
+
+        IPage<Map<String, Object>> previewPage = earlyWarningMapper.selectDashboardHistoryPage(
+                new Page<>(1, PREVIEW_LIMIT), null, null, null);
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("totalCount", numberValue(summary, "total_count", "totalCount"));
+        result.put("activeCount", numberValue(summary, "active_count", "activeCount"));
+        result.put("resolvedCount", numberValue(summary, "resolved_count", "resolvedCount"));
+        result.put("specialStats", specialStats);
+        result.put("detailList", normalizeRecords(previewPage == null ? null : previewPage.getRecords()));
+        result.put("previewLimit", PREVIEW_LIMIT);
+        result.put("generatedAt", LocalDateTime.now().format(DATE_TIME_FORMATTER));
+        return result;
+    }
+
+    @Override
+    public IPage<Map<String, Object>> getHistoryWarningPage(
+            Page<Map<String, Object>> page,
+            String warningType,
+            String warningSpecial,
+            String keyword) {
+        IPage<Map<String, Object>> result = earlyWarningMapper.selectDashboardHistoryPage(
+                page,
+                trimToNull(warningType),
+                trimToNull(warningSpecial),
+                trimToNull(keyword));
+        if (result == null) {
+            result = page;
+            result.setTotal(0);
+        }
+        result.setRecords(normalizeRecords(result.getRecords()));
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> getTodayUnprocessed(
+            Page<Map<String, Object>> page,
+            String warningType,
+            String warningLevel,
+            String keyword) {
+        IPage<Map<String, Object>> resultPage = earlyWarningMapper.selectDashboardUnprocessedPage(
+                page,
+                trimToNull(warningType),
+                trimToNull(warningLevel),
+                trimToNull(keyword));
+        if (resultPage == null) {
+            resultPage = page;
+            resultPage.setTotal(0);
+        }
+        List<Map<String, Object>> records = normalizeRecords(resultPage.getRecords());
+        resultPage.setRecords(records);
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("total", resultPage.getTotal());
+        result.put("list", records);
+        result.put("records", records);
+        result.put("pageNum", resultPage.getCurrent());
+        result.put("current", resultPage.getCurrent());
+        result.put("pageSize", resultPage.getSize());
+        result.put("size", resultPage.getSize());
+        result.put("pages", resultPage.getPages());
+        Map<String, Object> summary = nullToEmpty(earlyWarningMapper.selectDashboardUnprocessedSummary(
+                trimToNull(warningType), trimToNull(warningLevel), trimToNull(keyword)));
+        result.put("ownershipCount", numberValue(summary, "ownership_count", "ownershipCount"));
+        result.put("highestLevel", numberValue(summary, "highest_level", "highestLevel"));
+        result.put("generatedAt", LocalDateTime.now().format(DATE_TIME_FORMATTER));
+        return result;
+    }
+
+    private String normalizeCategory(String category) {
+        String value = trimToNull(category);
+        if (value == null) {
+            return CATEGORY_UNRESOLVED;
+        }
+        value = value.toLowerCase(Locale.ROOT);
+        if (!CATEGORY_UNRESOLVED.equals(value) && !CATEGORY_RESOLVED.equals(value)
+                && !CATEGORY_ALL.equals(value)) {
+            throw new IllegalArgumentException("category仅支持unresolved、resolved或all");
+        }
+        return value;
+    }
+
+    private List<Map<String, Object>> normalizeRecords(List<Map<String, Object>> records) {
+        if (records == null || records.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Map<String, Object>> result = new ArrayList<>(records.size());
+        for (Map<String, Object> record : records) {
+            result.add(normalizeRecord(record));
+        }
+        return result;
+    }
+
+    private Map<String, Object> normalizeRecord(Map<String, Object> source) {
+        Map<String, Object> record = new LinkedHashMap<>();
+        if (source != null) {
+            record.putAll(source);
+        }
+        putAlias(record, source, "warningId", "warning_id");
+        putAlias(record, source, "warningNo", "warning_no");
+        putAlias(record, source, "warningName", "warning_name");
+        putAlias(record, source, "warningType", "warning_type");
+        putAlias(record, source, "warningTypeName", "warning_type_name");
+        putAlias(record, source, "warningLevel", "warning_level");
+        putAlias(record, source, "warningSpecial", "warning_special");
+        putAlias(record, source, "ownershipUnit", "ownership_unit");
+        putAlias(record, source, "publishTime", "publish_time");
+        putAlias(record, source, "warningContent", "warning_content");
+        putAlias(record, source, "createTime", "create_time");
+        putAlias(record, source, "updateTime", "update_time");
+        putAlias(record, source, "deviceCode", "device_code");
+        putAlias(record, source, "deviceName", "device_name");
+        putAlias(record, source, "resolvedTime", "resolved_time");
+        putAlias(record, source, "waitingHours", "waiting_hours");
+
+        Object todoId = value(source, "todo_id", "todoId");
+        if (todoId != null) {
+            Map<String, Object> todoInfo = new LinkedHashMap<>();
+            todoInfo.put("todoId", todoId);
+            todoInfo.put("userId", value(source, "todo_user_id", "todoUserId"));
+            todoInfo.put("userName", value(source, "todo_user_name", "todoUserName"));
+            todoInfo.put("todoType", value(source, "todo_type", "todoType"));
+            todoInfo.put("readStatus", value(source, "read_status", "readStatus"));
+            todoInfo.put("taskName", value(source, "task_name", "taskName"));
+            todoInfo.put("createTime", value(source, "todo_create_time", "todoCreateTime"));
+            record.put("todoInfo", todoInfo);
+        }
+        return record;
+    }
+
+    private void putAlias(
+            Map<String, Object> target,
+            Map<String, Object> source,
+            String alias,
+            String... names) {
+        Object found = value(source, names);
+        if (found != null) {
+            target.put(alias, found);
+        }
+    }
+
+    private Map<String, Object> nullToEmpty(Map<String, Object> value) {
+        return value == null ? Collections.emptyMap() : value;
+    }
+
+    private long numberValue(Map<String, Object> source, String... names) {
+        Object found = value(source, names);
+        if (found instanceof Number) {
+            return ((Number) found).longValue();
+        }
+        if (found != null) {
+            try {
+                return new BigDecimal(found.toString()).longValue();
+            } catch (NumberFormatException ignored) {
+                log.debug("Unable to parse dashboard count: {}", found);
+            }
+        }
+        return 0L;
+    }
+
+    private BigDecimal decimalValue(Map<String, Object> source, String... names) {
+        Object found = value(source, names);
+        if (found instanceof BigDecimal) {
+            return (BigDecimal) found;
+        }
+        if (found instanceof Number) {
+            return BigDecimal.valueOf(((Number) found).doubleValue());
+        }
+        if (found != null) {
+            try {
+                return new BigDecimal(found.toString());
+            } catch (NumberFormatException ignored) {
+                log.debug("Unable to parse dashboard decimal: {}", found);
+            }
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private String stringValue(Map<String, Object> source, String... names) {
+        Object found = value(source, names);
+        return found == null ? "" : found.toString();
+    }
+
+    private Object value(Map<String, Object> source, String... names) {
+        if (source == null || source.isEmpty()) {
+            return null;
+        }
+        for (String name : names) {
+            if (source.containsKey(name)) {
+                return source.get(name);
+            }
+            for (Map.Entry<String, Object> entry : source.entrySet()) {
+                if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(name)) {
+                    return entry.getValue();
+                }
+            }
+        }
+        return null;
+    }
+
+    private String trimToNull(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+}

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

@@ -12,12 +12,19 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 @Slf4j
 @Service
 public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, WarningItem> implements WarningItemService {
 
+    private static final Set<String> ALLOWED_STATUSES =
+            new HashSet<>(Arrays.asList("ENABLED", "DISABLED", "INVALID"));
+
     @Override
     public IPage<WarningItem> queryWarningItems(Page<WarningItem> page, String itemCode, String itemName, 
                                                  String warningType, String warningLevel, String warningSpecial, String status) {
@@ -49,27 +56,45 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean addWarningItem(WarningItem warningItem) {
+        validateWarningItem(warningItem, false);
         if (warningItem.getItemCode() == null || warningItem.getItemCode().isEmpty()) {
             warningItem.setItemCode(generateItemCode(warningItem.getWarningType(), warningItem.getWarningLevel()));
         }
+        if (getItemByCode(warningItem.getItemCode()) != null) {
+            throw new IllegalArgumentException("事项编码已存在:" + warningItem.getItemCode());
+        }
         if (warningItem.getStatus() == null) {
             warningItem.setStatus("ENABLED");
         }
+        warningItem.setCreateTime(LocalDateTime.now());
         return this.save(warningItem);
     }
 
     @Override
     @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.setUpdateTime(LocalDateTime.now());
         return this.updateById(warningItem);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean updateItemStatus(String itemId, String status) {
+        if (itemId == null || itemId.trim().isEmpty()) {
+            throw new IllegalArgumentException("事项ID不能为空");
+        }
+        if (!ALLOWED_STATUSES.contains(status)) {
+            throw new IllegalArgumentException("不支持的事项状态:" + status);
+        }
         WarningItem item = new WarningItem();
         item.setItemId(itemId);
         item.setStatus(status);
+        item.setUpdateTime(LocalDateTime.now());
         return this.updateById(item);
     }
 
@@ -87,6 +112,14 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
 
     @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();
         String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
         String prefix = "WI" + warningType.substring(0, Math.min(3, warningType.length())).toUpperCase() + warningLevel;
         
@@ -100,14 +133,40 @@ public class WarningItemServiceImpl extends ServiceImpl<WarningItemMapper, Warni
         
         if (lastItem != null) {
             String lastCode = lastItem.getItemCode();
-            String lastSeqStr = lastCode.substring(lastCode.length() - 4);
-            try {
-                sequence = Integer.parseInt(lastSeqStr) + 1;
-            } catch (NumberFormatException e) {
-                sequence = 1;
+            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);
     }
+
+    private void validateWarningItem(WarningItem warningItem, boolean update) {
+        if (warningItem == null) {
+            throw new IllegalArgumentException("预警事项不能为空");
+        }
+        if (update && (warningItem.getItemId() == null || warningItem.getItemId().trim().isEmpty())) {
+            throw new IllegalArgumentException("事项ID不能为空");
+        }
+        if (warningItem.getItemName() == null || warningItem.getItemName().trim().isEmpty()) {
+            throw new IllegalArgumentException("事项名称不能为空");
+        }
+        if (warningItem.getWarningType() == null || warningItem.getWarningType().trim().isEmpty()) {
+            throw new IllegalArgumentException("预警类型不能为空");
+        }
+        if (warningItem.getWarningLevel() == null || warningItem.getWarningLevel().trim().isEmpty()) {
+            throw new IllegalArgumentException("预警级别不能为空");
+        }
+        if (warningItem.getDuration() != null && warningItem.getDuration() <= 0) {
+            throw new IllegalArgumentException("预警时长必须大于0");
+        }
+        if (warningItem.getStatus() != null && !ALLOWED_STATUSES.contains(warningItem.getStatus())) {
+            throw new IllegalArgumentException("不支持的事项状态:" + warningItem.getStatus());
+        }
+    }
 }

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

@@ -12,13 +12,19 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @Slf4j
 @Service
 public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, WarningReason> implements WarningReasonService {
 
+    private static final Set<String> ALLOWED_STATUSES = new HashSet<>(Arrays.asList("0", "1"));
+
     @Override
     public IPage<WarningReason> queryWarningReasons(Page<WarningReason> page, String reasonCode, String reasonName, 
                                                      String warningType, String warningLevel, String status) {
@@ -57,30 +63,48 @@ public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, W
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean addWarningReason(WarningReason warningReason) {
+        validateWarningReason(warningReason, false);
         if (warningReason.getReasonCode() == null || warningReason.getReasonCode().isEmpty()) {
             warningReason.setReasonCode(generateReasonCode(warningReason.getWarningType(), warningReason.getWarningLevel()));
         }
+        if (getReasonByCode(warningReason.getReasonCode()) != null) {
+            throw new IllegalArgumentException("原因编码已存在:" + warningReason.getReasonCode());
+        }
         if (warningReason.getStatus() == null) {
             warningReason.setStatus("0");
         }
         if (warningReason.getSortOrder() == null) {
             warningReason.setSortOrder(0);
         }
+        warningReason.setCreateTime(LocalDateTime.now());
         return this.save(warningReason);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean updateWarningReason(WarningReason warningReason) {
+        validateWarningReason(warningReason, true);
+        WarningReason sameCodeReason = getReasonByCode(warningReason.getReasonCode());
+        if (sameCodeReason != null && !sameCodeReason.getReasonId().equals(warningReason.getReasonId())) {
+            throw new IllegalArgumentException("原因编码已存在:" + warningReason.getReasonCode());
+        }
+        warningReason.setUpdateTime(LocalDateTime.now());
         return this.updateById(warningReason);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean updateReasonStatus(String reasonId, String status) {
+        if (reasonId == null || reasonId.trim().isEmpty()) {
+            throw new IllegalArgumentException("原因ID不能为空");
+        }
+        if (!ALLOWED_STATUSES.contains(status)) {
+            throw new IllegalArgumentException("不支持的原因状态:" + status);
+        }
         WarningReason reason = new WarningReason();
         reason.setReasonId(reasonId);
         reason.setStatus(status);
+        reason.setUpdateTime(LocalDateTime.now());
         return this.updateById(reason);
     }
 
@@ -90,6 +114,14 @@ public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, W
     }
 
     private String generateReasonCode(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();
         String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
         String prefix = "WR" + warningType.substring(0, Math.min(3, warningType.length())).toUpperCase() + warningLevel;
         
@@ -103,14 +135,49 @@ public class WarningReasonServiceImpl extends ServiceImpl<WarningReasonMapper, W
         
         if (lastReason != null) {
             String lastCode = lastReason.getReasonCode();
-            String lastSeqStr = lastCode.substring(lastCode.length() - 4);
-            try {
-                sequence = Integer.parseInt(lastSeqStr) + 1;
-            } catch (NumberFormatException e) {
-                sequence = 1;
+            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);
     }
+
+    private WarningReason getReasonByCode(String reasonCode) {
+        if (reasonCode == null || reasonCode.trim().isEmpty()) {
+            return null;
+        }
+        LambdaQueryWrapper<WarningReason> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WarningReason::getReasonCode, reasonCode.trim());
+        return this.getOne(wrapper);
+    }
+
+    private void validateWarningReason(WarningReason warningReason, boolean update) {
+        if (warningReason == null) {
+            throw new IllegalArgumentException("预警原因不能为空");
+        }
+        if (update && (warningReason.getReasonId() == null || warningReason.getReasonId().trim().isEmpty())) {
+            throw new IllegalArgumentException("原因ID不能为空");
+        }
+        if (warningReason.getReasonName() == null || warningReason.getReasonName().trim().isEmpty()) {
+            throw new IllegalArgumentException("原因名称不能为空");
+        }
+        if (warningReason.getWarningType() == null || warningReason.getWarningType().trim().isEmpty()) {
+            throw new IllegalArgumentException("预警类型不能为空");
+        }
+        if (warningReason.getWarningLevel() == null || warningReason.getWarningLevel().trim().isEmpty()) {
+            throw new IllegalArgumentException("预警级别不能为空");
+        }
+        if (warningReason.getSortOrder() != null && warningReason.getSortOrder() < 0) {
+            throw new IllegalArgumentException("排序值不能小于0");
+        }
+        if (warningReason.getStatus() != null && !ALLOWED_STATUSES.contains(warningReason.getStatus())) {
+            throw new IllegalArgumentException("不支持的原因状态:" + warningReason.getStatus());
+        }
+    }
 }

+ 8 - 5
pipe-network-service/zksy-system/src/main/java/com/zksy/system/service/impl/SysMenuServiceImpl.java

@@ -412,7 +412,14 @@ public class SysMenuServiceImpl implements ISysMenuService
     public String getComponent(SysMenu menu)
     {
         String component = UserConstants.LAYOUT;
-        if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu))
+        // 非顶级目录不能再次加载 Layout,否则会在现有系统框架内嵌套一整套导航和标签栏。
+        // 对历史菜单中误配置为 Layout 的记录做兼容,统一按 ParentView 构建嵌套路由。
+        if (isParentView(menu) && (StringUtils.isEmpty(menu.getComponent())
+                || UserConstants.LAYOUT.equals(menu.getComponent())))
+        {
+            component = UserConstants.PARENT_VIEW;
+        }
+        else if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu))
         {
             component = menu.getComponent();
         }
@@ -420,10 +427,6 @@ public class SysMenuServiceImpl implements ISysMenuService
         {
             component = UserConstants.INNER_LINK;
         }
-        else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu))
-        {
-            component = UserConstants.PARENT_VIEW;
-        }
         return component;
     }
 

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

@@ -36,23 +36,116 @@
         update_time, remark
     </sql>
 
+    <!-- The city-life project only contains water supply, drainage, gas and manhole warnings. -->
+    <sql id="SupportedDashboardWarningTypes">
+        AND w.warning_type IN (
+            'WATER_PIPE', 'SEWER_PIPE', 'GAS_PIPE', 'MANHOLE',
+            '1', '2', '3', '4',
+            '供水', '供水管网', '供水管网预警',
+            '排水', '排水管网', '排水管网预警',
+            '燃气', '燃气管网', '燃气管网预警',
+            '窨井', '窨井预警'
+        )
+    </sql>
+
+    <sql id="DashboardWarningTypeFilter">
+        <if test="warningType != null and warningType != ''">
+            <choose>
+                <when test="warningType == 'WATER_PIPE' or warningType == '1'.toString()">
+                    AND w.warning_type IN ('WATER_PIPE', '1', '供水', '供水管网', '供水管网预警')
+                </when>
+                <when test="warningType == 'SEWER_PIPE' or warningType == '2'.toString()">
+                    AND w.warning_type IN ('SEWER_PIPE', '2', '排水', '排水管网', '排水管网预警')
+                </when>
+                <when test="warningType == 'GAS_PIPE' or warningType == '3'.toString()">
+                    AND w.warning_type IN ('GAS_PIPE', '3', '燃气', '燃气管网', '燃气管网预警')
+                </when>
+                <when test="warningType == 'MANHOLE' or warningType == '4'.toString()">
+                    AND w.warning_type IN ('MANHOLE', '4', '窨井', '窨井预警')
+                </when>
+                <otherwise>
+                    AND w.warning_type = #{warningType}
+                </otherwise>
+            </choose>
+        </if>
+    </sql>
+
+    <sql id="DashboardWarningLevelFilter">
+        <if test="warningLevel != null and warningLevel != ''">
+            <choose>
+                <when test="warningLevel == '1'.toString()">
+                    AND w.warning_level IN ('1', 'BLUE', 'IV')
+                </when>
+                <when test="warningLevel == '2'.toString()">
+                    AND w.warning_level IN ('2', 'YELLOW', 'III')
+                </when>
+                <when test="warningLevel == '3'.toString()">
+                    AND w.warning_level IN ('3', 'ORANGE', 'II')
+                </when>
+                <when test="warningLevel == '4'.toString()">
+                    AND w.warning_level IN ('4', 'RED', 'I')
+                </when>
+                <otherwise>
+                    AND w.warning_level = #{warningLevel}
+                </otherwise>
+            </choose>
+        </if>
+    </sql>
+
+    <sql id="DashboardWarningColumns">
+        w.warning_id,
+        w.warning_no,
+        w.warning_name,
+        w.warning_type,
+        CASE
+            WHEN w.warning_type IN ('WATER_PIPE', '1', '供水', '供水管网', '供水管网预警') THEN '供水管网'
+            WHEN w.warning_type IN ('SEWER_PIPE', '2', '排水', '排水管网', '排水管网预警') THEN '排水管网'
+            WHEN w.warning_type IN ('GAS_PIPE', '3', '燃气', '燃气管网', '燃气管网预警') THEN '燃气管网'
+            WHEN w.warning_type IN ('MANHOLE', '4', '窨井', '窨井预警') THEN '窨井'
+        END AS warning_type_name,
+        w.warning_level,
+        w.warning_special,
+        w.location,
+        w.longitude,
+        w.latitude,
+        w.ownership_unit,
+        w.publisher,
+        w.publish_time,
+        w.handler,
+        w.status,
+        w.warning_content,
+        w.create_time,
+        w.update_time,
+        w.device_code,
+        w.device_name
+    </sql>
+
+    <sql id="DashboardResolvedTimeJoin">
+        LEFT JOIN (
+            SELECT warning_id, MAX(create_time) AS resolved_time
+            FROM app_user.warning_disposal
+            WHERE disposal_type IN ('RESOLVE', 'CLOSE', 'CLEAR')
+            GROUP BY warning_id
+        ) resolved_record ON resolved_record.warning_id = w.warning_id
+    </sql>
+
     <!-- 统计今日预警核心指标 -->
     <select id="selectTodayWarningStats" resultType="java.util.Map">
         SELECT
         COUNT(CASE WHEN status != 'DRAFT' THEN 1 END) AS today_total,
         COUNT(CASE WHEN status = 'HANDLED' THEN 1 END) AS handled_today,
         COUNT(CASE WHEN status = 'CLOSED' THEN 1 END) AS closed_today
-        FROM early_warning
+        FROM app_user.early_warning
         WHERE publish_time &gt;= CURRENT_DATE
-        AND publish_time &lt; CURRENT_DATE + INTERVAL '1 day'
+        AND publish_time &lt; CAST(CURRENT_DATE AS TIMESTAMP) + CAST('1 day' AS INTERVAL)
     </select>
 
     <!-- 查询今日预警列表 -->
     <select id="selectTodayWarningList" resultMap="BaseResultMap">
         SELECT <include refid="Base_Column_List"/>
-        FROM early_warning
+        FROM app_user.early_warning
         WHERE publish_time &gt;= CURRENT_DATE
-        AND publish_time &lt; CURRENT_DATE + INTERVAL '1 day'
+        AND publish_time &lt; CAST(CURRENT_DATE AS TIMESTAMP) + CAST('1 day' AS INTERVAL)
         <choose>
             <when test="status != null and status != ''">
                 AND status = #{status}
@@ -70,8 +163,9 @@
         TO_CHAR(publish_time, 'YYYY-MM-DD') AS stat_date,
         COALESCE(warning_special, '其他') AS warning_special,
         COUNT(*) AS warning_count
-        FROM early_warning
+        FROM app_user.early_warning
         WHERE status != 'DRAFT'
+        AND warning_type IN ('WATER_PIPE', 'SEWER_PIPE', 'GAS_PIPE', 'MANHOLE', '1', '2', '3', '4', '供水', '排水', '燃气', '窨井')
         <if test="startTime != null">
             AND publish_time &gt;= #{startTime}
         </if>
@@ -91,8 +185,9 @@
         TO_CHAR(publish_time, 'YYYY-MM') AS stat_month,
         COALESCE(warning_special, '其他') AS warning_special,
         COUNT(*) AS warning_count
-        FROM early_warning
+        FROM app_user.early_warning
         WHERE status != 'DRAFT'
+        AND warning_type IN ('WATER_PIPE', 'SEWER_PIPE', 'GAS_PIPE', 'MANHOLE', '1', '2', '3', '4', '供水', '排水', '燃气', '窨井')
         <if test="startTime != null">
             AND publish_time &gt;= #{startTime}
         </if>
@@ -111,8 +206,16 @@
         SELECT
         status,
         COUNT(warning_id) AS status_count
-        FROM early_warning
+        FROM app_user.early_warning
         WHERE status != 'DRAFT'
+        AND warning_type IN (
+            'WATER_PIPE', 'SEWER_PIPE', 'GAS_PIPE', 'MANHOLE',
+            '1', '2', '3', '4',
+            '供水', '供水管网', '供水管网预警',
+            '排水', '排水管网', '排水管网预警',
+            '燃气', '燃气管网', '燃气管网预警',
+            '窨井', '窨井预警'
+        )
         <if test="startTime != null">
             AND publish_time &gt;= #{startTime}
         </if>
@@ -126,6 +229,24 @@
         ORDER BY status_count DESC
     </select>
 
+    <select id="selectDisposalGroupByType" resultType="java.util.Map">
+        SELECT d.disposal_type, COUNT(*) AS disposal_count
+        FROM app_user.warning_disposal d
+        INNER JOIN app_user.early_warning w ON w.warning_id = d.warning_id
+        WHERE w.status != 'DRAFT'
+        <if test="startTime != null">
+            AND w.publish_time &gt;= #{startTime}
+        </if>
+        <if test="endTime != null">
+            AND w.publish_time &lt; #{endTime}
+        </if>
+        <if test="warningSpecial != null and warningSpecial != ''">
+            AND w.warning_special = #{warningSpecial}
+        </if>
+        GROUP BY d.disposal_type
+        ORDER BY disposal_count DESC
+    </select>
+
     <!-- 处置时效分析-概览指标统计 -->
     <select id="selectDisposeEfficiencyOverview" resultType="java.util.Map">
         SELECT
@@ -135,9 +256,9 @@
         COUNT(*) AS total_count
         FROM (
         SELECT
-        EXTRACT(EPOCH FROM (MAX(d.create_time) - w.publish_time)) / 3600 AS dispose_hours
-        FROM early_warning w
-        LEFT JOIN warning_disposal d ON w.warning_id = d.warning_id
+        EXTRACT(EPOCH FROM (CAST(MAX(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600 AS dispose_hours
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status IN ('HANDLED', 'CLOSED')
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -148,8 +269,8 @@
         <if test="warningSpecial != null and warningSpecial != ''">
             AND w.warning_special = #{warningSpecial}
         </if>
-        GROUP BY w.warning_id
-        HAVING dispose_hours IS NOT NULL
+        GROUP BY w.warning_id, w.publish_time
+        HAVING MAX(d.create_time) &gt;= w.publish_time
         ) AS temp
     </select>
 
@@ -165,9 +286,9 @@
         COUNT(*) AS warning_count
         FROM (
         SELECT
-        EXTRACT(EPOCH FROM (MAX(d.create_time) - w.publish_time)) / 3600 AS dispose_hours
-        FROM early_warning w
-        LEFT JOIN warning_disposal d ON w.warning_id = d.warning_id
+        EXTRACT(EPOCH FROM (CAST(MAX(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600 AS dispose_hours
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status IN ('HANDLED', 'CLOSED')
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -178,8 +299,8 @@
         <if test="warningSpecial != null and warningSpecial != ''">
             AND w.warning_special = #{warningSpecial}
         </if>
-        GROUP BY w.warning_id
-        HAVING dispose_hours IS NOT NULL
+        GROUP BY w.warning_id, w.publish_time
+        HAVING MAX(d.create_time) &gt;= w.publish_time
         ) AS temp
         GROUP BY time_range
         ORDER BY
@@ -200,9 +321,9 @@
         FROM (
         SELECT
         w.warning_special,
-        EXTRACT(EPOCH FROM (MAX(d.create_time) - w.publish_time)) / 3600 AS dispose_hours
-        FROM early_warning w
-        LEFT JOIN warning_disposal d ON w.warning_id = d.warning_id
+        EXTRACT(EPOCH FROM (CAST(MAX(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600 AS dispose_hours
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status IN ('HANDLED', 'CLOSED')
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -210,8 +331,11 @@
         <if test="endTime != null">
             AND w.publish_time &lt; #{endTime}
         </if>
-        GROUP BY w.warning_id
-        HAVING dispose_hours IS NOT NULL
+        <if test="warningSpecial != null and warningSpecial != ''">
+            AND w.warning_special = #{warningSpecial}
+        </if>
+        GROUP BY w.warning_id, w.warning_special, w.publish_time
+        HAVING MAX(d.create_time) &gt;= w.publish_time
         ) AS temp
         GROUP BY warning_special
         ORDER BY avg_dispose_hours DESC
@@ -226,10 +350,10 @@
         COALESCE(w.warning_special, '其他') AS warning_special,
         w.publish_time,
         MAX(d.create_time) AS dispose_time,
-        ROUND(EXTRACT(EPOCH FROM (MAX(d.create_time) - w.publish_time)) / 3600, 2) AS dispose_hours,
+        ROUND(EXTRACT(EPOCH FROM (CAST(MAX(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600, 2) AS dispose_hours,
         w.status
-        FROM early_warning w
-        LEFT JOIN warning_disposal d ON w.warning_id = d.warning_id
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status IN ('HANDLED', 'CLOSED')
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -240,8 +364,8 @@
         <if test="warningSpecial != null and warningSpecial != ''">
             AND w.warning_special = #{warningSpecial}
         </if>
-        GROUP BY w.warning_id
-        HAVING dispose_hours IS NOT NULL
+        GROUP BY w.warning_id, w.warning_no, w.warning_name, w.warning_special, w.publish_time, w.status
+        HAVING MAX(d.create_time) &gt;= w.publish_time
         ORDER BY dispose_hours
         <if test="sortType != null and sortType == 'desc'">
             DESC
@@ -260,9 +384,9 @@
         COUNT(*) AS total_count
         FROM (
         SELECT
-        EXTRACT(EPOCH FROM (MIN(d.create_time) - w.publish_time)) / 3600 AS receive_hours
-        FROM early_warning w
-        INNER JOIN warning_disposal d ON w.warning_id = d.warning_id
+        EXTRACT(EPOCH FROM (CAST(MIN(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600 AS receive_hours
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status != 'DRAFT'
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -279,8 +403,8 @@
         <if test="status != null and status != ''">
             AND w.status = #{status}
         </if>
-        GROUP BY w.warning_id
-        HAVING receive_hours IS NOT NULL
+        GROUP BY w.warning_id, w.publish_time
+        HAVING MIN(d.create_time) &gt;= w.publish_time
         ) AS temp
     </select>
 
@@ -296,9 +420,9 @@
         COUNT(*) AS warning_count
         FROM (
         SELECT
-        EXTRACT(EPOCH FROM (MIN(d.create_time) - w.publish_time)) / 3600 AS receive_hours
-        FROM early_warning w
-        INNER JOIN warning_disposal d ON w.warning_id = d.warning_id
+        EXTRACT(EPOCH FROM (CAST(MIN(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600 AS receive_hours
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status != 'DRAFT'
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -315,8 +439,8 @@
         <if test="status != null and status != ''">
             AND w.status = #{status}
         </if>
-        GROUP BY w.warning_id
-        HAVING receive_hours IS NOT NULL
+        GROUP BY w.warning_id, w.publish_time
+        HAVING MIN(d.create_time) &gt;= w.publish_time
         ) AS temp
         GROUP BY time_range
         ORDER BY
@@ -337,9 +461,9 @@
         FROM (
         SELECT
         w.warning_type,
-        EXTRACT(EPOCH FROM (MIN(d.create_time) - w.publish_time)) / 3600 AS receive_hours
-        FROM early_warning w
-        INNER JOIN warning_disposal d ON w.warning_id = d.warning_id
+        EXTRACT(EPOCH FROM (CAST(MIN(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600 AS receive_hours
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status != 'DRAFT'
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -347,8 +471,17 @@
         <if test="endTime != null">
             AND w.publish_time &lt; #{endTime}
         </if>
-        GROUP BY w.warning_id
-        HAVING receive_hours IS NOT NULL
+        <if test="warningType != null and warningType != ''">
+            AND w.warning_type = #{warningType}
+        </if>
+        <if test="warningSpecial != null and warningSpecial != ''">
+            AND w.warning_special = #{warningSpecial}
+        </if>
+        <if test="status != null and status != ''">
+            AND w.status = #{status}
+        </if>
+        GROUP BY w.warning_id, w.warning_type, w.publish_time
+        HAVING MIN(d.create_time) &gt;= w.publish_time
         ) AS temp
         GROUP BY warning_type
         ORDER BY avg_receive_hours DESC
@@ -364,10 +497,10 @@
         COALESCE(w.warning_special, '其他') AS warning_special,
         w.publish_time,
         MIN(d.create_time) AS receive_time,
-        ROUND(EXTRACT(EPOCH FROM (MIN(d.create_time) - w.publish_time)) / 3600, 2) AS receive_hours,
+        ROUND(EXTRACT(EPOCH FROM (CAST(MIN(d.create_time) AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ))) / 3600, 2) AS receive_hours,
         w.status
-        FROM early_warning w
-        INNER JOIN warning_disposal d ON w.warning_id = d.warning_id
+        FROM app_user.early_warning w
+        INNER JOIN app_user.warning_disposal d ON w.warning_id = d.warning_id
         WHERE w.status != 'DRAFT'
         <if test="startTime != null">
             AND w.publish_time &gt;= #{startTime}
@@ -384,8 +517,9 @@
         <if test="status != null and status != ''">
             AND w.status = #{status}
         </if>
-        GROUP BY w.warning_id
-        HAVING receive_hours IS NOT NULL
+        GROUP BY w.warning_id, w.warning_no, w.warning_name, w.warning_type,
+                 w.warning_special, w.publish_time, w.status
+        HAVING MIN(d.create_time) &gt;= w.publish_time
         ORDER BY receive_hours
         <if test="sortType != null and sortType == 'desc'">
             DESC
@@ -397,6 +531,291 @@
 
     <!-- ————————————————————————————————————————————————————————————— -->
 
+    <!-- Warning overview: headline numbers for current active and resolved-today warnings. -->
+    <select id="selectDashboardTodaySummary" resultType="java.util.Map">
+        SELECT
+            COUNT(CASE
+                WHEN w.publish_time &gt;= CURRENT_DATE
+                 AND w.publish_time &lt; CAST(CURRENT_DATE AS TIMESTAMP) + CAST('1 day' AS INTERVAL)
+                 AND w.publish_time &lt;= CURRENT_TIMESTAMP
+                 AND w.status != 'DRAFT'
+                THEN 1 END) AS today_total,
+            COUNT(CASE
+                WHEN w.publish_time &lt;= CURRENT_TIMESTAMP
+                 AND w.status NOT IN ('DRAFT', 'CLOSED', 'RESOLVED')
+                THEN 1 END) AS unresolved_count,
+            COUNT(CASE
+                WHEN w.status IN ('CLOSED', 'RESOLVED')
+                 AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) &gt;= CURRENT_DATE
+                 AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time)
+                     &lt; CAST(CURRENT_DATE AS TIMESTAMP) + CAST('1 day' AS INTERVAL)
+                 AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) &lt;= CURRENT_TIMESTAMP
+                THEN 1 END) AS resolved_count
+        FROM app_user.early_warning w
+        <include refid="DashboardResolvedTimeJoin"/>
+        WHERE 1 = 1
+        <include refid="SupportedDashboardWarningTypes"/>
+    </select>
+
+    <!-- Small detail lists returned with the today overview cards. -->
+    <select id="selectDashboardTodayPreview" resultType="java.util.Map">
+        SELECT
+            <include refid="DashboardWarningColumns"/>,
+            COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) AS resolved_time,
+            ROUND(
+                EXTRACT(EPOCH FROM (
+                    CAST(CURRENT_TIMESTAMP AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ)
+                )) / 3600,
+                2
+            ) AS waiting_hours
+        FROM app_user.early_warning w
+        <include refid="DashboardResolvedTimeJoin"/>
+        WHERE 1 = 1
+        <include refid="SupportedDashboardWarningTypes"/>
+        <choose>
+            <when test="category == 'resolved'">
+                AND w.status IN ('CLOSED', 'RESOLVED')
+                AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) &gt;= CURRENT_DATE
+                AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time)
+                    &lt; CAST(CURRENT_DATE AS TIMESTAMP) + CAST('1 day' AS INTERVAL)
+                AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) &lt;= CURRENT_TIMESTAMP
+                ORDER BY resolved_time DESC, w.warning_id DESC
+            </when>
+            <otherwise>
+                AND w.publish_time &lt;= CURRENT_TIMESTAMP
+                AND w.status NOT IN ('DRAFT', 'CLOSED', 'RESOLVED')
+                ORDER BY
+                    CASE w.warning_level
+                        WHEN '4' THEN 1 WHEN 'RED' THEN 1
+                        WHEN '3' THEN 2 WHEN 'ORANGE' THEN 2
+                        WHEN '2' THEN 3 WHEN 'YELLOW' THEN 3
+                        WHEN '1' THEN 4 WHEN 'BLUE' THEN 4
+                        ELSE 5
+                    END,
+                    w.publish_time ASC,
+                    w.warning_id ASC
+            </otherwise>
+        </choose>
+        LIMIT #{limit}
+    </select>
+
+    <!-- Paged detail list for the two today overview categories. -->
+    <select id="selectDashboardTodayPage" resultType="java.util.Map">
+        SELECT
+            <include refid="DashboardWarningColumns"/>,
+            COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) AS resolved_time,
+            ROUND(
+                EXTRACT(EPOCH FROM (
+                    CAST(CURRENT_TIMESTAMP AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ)
+                )) / 3600,
+                2
+            ) AS waiting_hours
+        FROM app_user.early_warning w
+        <include refid="DashboardResolvedTimeJoin"/>
+        <where>
+            <include refid="SupportedDashboardWarningTypes"/>
+            <choose>
+                <when test="category == 'resolved'">
+                    AND w.status IN ('CLOSED', 'RESOLVED')
+                    AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) &gt;= CURRENT_DATE
+                    AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time)
+                        &lt; CAST(CURRENT_DATE AS TIMESTAMP) + CAST('1 day' AS INTERVAL)
+                    AND COALESCE(resolved_record.resolved_time, w.update_time, w.publish_time) &lt;= CURRENT_TIMESTAMP
+                </when>
+                <when test="category == 'all'">
+                    AND w.publish_time &gt;= CURRENT_DATE
+                    AND w.publish_time &lt; CAST(CURRENT_DATE AS TIMESTAMP) + CAST('1 day' AS INTERVAL)
+                    AND w.publish_time &lt;= CURRENT_TIMESTAMP
+                    AND w.status != 'DRAFT'
+                </when>
+                <otherwise>
+                    AND w.publish_time &lt;= CURRENT_TIMESTAMP
+                    AND w.status NOT IN ('DRAFT', 'CLOSED', 'RESOLVED')
+                </otherwise>
+            </choose>
+            <include refid="DashboardWarningTypeFilter"/>
+            <include refid="DashboardWarningLevelFilter"/>
+            <if test="keyword != null and keyword != ''">
+                AND (
+                    w.warning_no LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.warning_name LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.warning_special LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.location LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.ownership_unit LIKE CONCAT('%', #{keyword}, '%')
+                )
+            </if>
+        </where>
+        <choose>
+            <when test="category == 'resolved'">
+                ORDER BY resolved_time DESC, w.warning_id DESC
+            </when>
+            <when test="category == 'all'">
+                ORDER BY w.publish_time DESC, w.warning_id DESC
+            </when>
+            <otherwise>
+                ORDER BY
+                    CASE w.warning_level
+                        WHEN '4' THEN 1 WHEN 'RED' THEN 1
+                        WHEN '3' THEN 2 WHEN 'ORANGE' THEN 2
+                        WHEN '2' THEN 3 WHEN 'YELLOW' THEN 3
+                        WHEN '1' THEN 4 WHEN 'BLUE' THEN 4
+                        ELSE 5
+                    END,
+                    w.publish_time ASC,
+                    w.warning_id ASC
+            </otherwise>
+        </choose>
+    </select>
+
+    <!-- Historical warning summary. Drafts and unsupported facility types are excluded. -->
+    <select id="selectDashboardHistorySummary" resultType="java.util.Map">
+        SELECT
+            COUNT(*) AS total_count,
+            COUNT(CASE WHEN w.status IN ('CLOSED', 'RESOLVED') THEN 1 END) AS resolved_count,
+            COUNT(CASE WHEN w.status NOT IN ('CLOSED', 'RESOLVED') THEN 1 END) AS active_count
+        FROM app_user.early_warning w
+        WHERE w.status != 'DRAFT'
+        AND w.publish_time &lt;= CURRENT_TIMESTAMP
+        <include refid="SupportedDashboardWarningTypes"/>
+    </select>
+
+    <select id="selectDashboardHistorySpecialStats" resultType="java.util.Map">
+        WITH special_stats AS (
+            SELECT
+                COALESCE(NULLIF(w.warning_special, ''), '未分类') AS special_name,
+                COUNT(*) AS warning_count
+            FROM app_user.early_warning w
+            WHERE w.status != 'DRAFT'
+            AND w.publish_time &lt;= CURRENT_TIMESTAMP
+            <include refid="SupportedDashboardWarningTypes"/>
+            <include refid="DashboardWarningTypeFilter"/>
+            GROUP BY COALESCE(NULLIF(w.warning_special, ''), '未分类')
+        )
+        SELECT
+            special_name,
+            warning_count,
+            ROUND(warning_count * 100.0 / NULLIF(SUM(warning_count) OVER (), 0), 2) AS percentage
+        FROM special_stats
+        ORDER BY warning_count DESC, special_name ASC
+    </select>
+
+    <select id="selectDashboardHistoryPage" resultType="java.util.Map">
+        SELECT <include refid="DashboardWarningColumns"/>
+        FROM app_user.early_warning w
+        <where>
+            AND w.status != 'DRAFT'
+            AND w.publish_time &lt;= CURRENT_TIMESTAMP
+            <include refid="SupportedDashboardWarningTypes"/>
+            <include refid="DashboardWarningTypeFilter"/>
+            <if test="warningSpecial != null and warningSpecial != ''">
+                AND COALESCE(NULLIF(w.warning_special, ''), '未分类') = #{warningSpecial}
+            </if>
+            <if test="keyword != null and keyword != ''">
+                AND (
+                    w.warning_no LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.warning_name LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.location LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.ownership_unit LIKE CONCAT('%', #{keyword}, '%')
+                )
+            </if>
+        </where>
+        ORDER BY w.publish_time DESC, w.warning_id DESC
+    </select>
+
+    <!-- Outstanding warning list. One most-recent open todo is attached per warning. -->
+    <select id="selectDashboardUnprocessedPage" resultType="java.util.Map">
+        SELECT
+            <include refid="DashboardWarningColumns"/>,
+            open_todo.todo_id,
+            open_todo.user_id AS todo_user_id,
+            open_todo.user_name AS todo_user_name,
+            open_todo.todo_type,
+            open_todo.read_status,
+            open_todo.task_name,
+            open_todo.create_time AS todo_create_time,
+            ROUND(
+                EXTRACT(EPOCH FROM (
+                    CAST(CURRENT_TIMESTAMP AS TIMESTAMPTZ) - CAST(w.publish_time AS TIMESTAMPTZ)
+                )) / 3600,
+                2
+            ) AS waiting_hours
+        FROM app_user.early_warning w
+        LEFT JOIN (
+            SELECT todo_id, warning_id, user_id, user_name, todo_type, read_status, task_name, create_time
+            FROM (
+                SELECT
+                    t.*,
+                    ROW_NUMBER() OVER (
+                        PARTITION BY t.warning_id
+                        ORDER BY t.create_time DESC, t.todo_id DESC
+                    ) AS row_num
+                FROM app_user.warning_todo t
+                WHERE t.complete_time IS NULL
+                AND (t.todo_type IS NULL OR t.todo_type != 'DONE')
+            ) ranked_todo
+            WHERE row_num = 1
+        ) 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')
+            <include refid="SupportedDashboardWarningTypes"/>
+            <include refid="DashboardWarningTypeFilter"/>
+            <include refid="DashboardWarningLevelFilter"/>
+            <if test="keyword != null and keyword != ''">
+                AND (
+                    w.warning_no LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.warning_name LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.warning_special LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.location LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.ownership_unit LIKE CONCAT('%', #{keyword}, '%')
+                )
+            </if>
+        </where>
+        ORDER BY
+            CASE w.warning_level
+                WHEN '4' THEN 1 WHEN 'RED' THEN 1
+                WHEN '3' THEN 2 WHEN 'ORANGE' THEN 2
+                WHEN '2' THEN 3 WHEN 'YELLOW' THEN 3
+                WHEN '1' THEN 4 WHEN 'BLUE' THEN 4
+                ELSE 5
+            END,
+            w.publish_time ASC,
+            w.warning_id ASC
+        </select>
+
+    <!-- Aggregate cards for the unprocessed list. The filters mirror the page
+         query so counts remain correct when the user searches or narrows a
+         warning type/level. -->
+    <select id="selectDashboardUnprocessedSummary" resultType="java.util.Map">
+        SELECT
+            COUNT(*) AS total,
+            COUNT(DISTINCT NULLIF(w.ownership_unit, '')) AS ownership_count,
+            MAX(CASE
+                WHEN w.warning_level IN ('1', 'BLUE', 'IV') THEN 1
+                WHEN w.warning_level IN ('2', 'YELLOW', 'III') THEN 2
+                WHEN w.warning_level IN ('3', 'ORANGE', 'II') THEN 3
+                WHEN w.warning_level IN ('4', 'RED', 'I') THEN 4
+                ELSE NULL
+            END) AS highest_level
+        FROM app_user.early_warning w
+        <where>
+            AND w.publish_time &lt;= CURRENT_TIMESTAMP
+            AND w.status IN ('PENDING', 'PROCESSING', 'RELEASED')
+            <include refid="SupportedDashboardWarningTypes"/>
+            <include refid="DashboardWarningTypeFilter"/>
+            <include refid="DashboardWarningLevelFilter"/>
+            <if test="keyword != null and keyword != ''">
+                AND (
+                    w.warning_no LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.warning_name LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.warning_special LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.location LIKE CONCAT('%', #{keyword}, '%')
+                    OR w.ownership_unit LIKE CONCAT('%', #{keyword}, '%')
+                )
+            </if>
+        </where>
+    </select>
+
     <select id="selectWarningList" resultType="java.util.Map">
         SELECT w.*
         FROM early_warning w

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

@@ -104,6 +104,7 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
     private EarlyWarning findOpenAutoWarning(String deviceCode, String warningCode) {
         String marker = buildRemarkMarker(deviceCode, warningCode);
         return earlyWarningApiMapper.selectOne(new LambdaQueryWrapper<EarlyWarning>()
+                .eq(EarlyWarning::getDeviceCode, deviceCode)
                 .eq(EarlyWarning::getRemark, marker)
                 .in(EarlyWarning::getStatus, OPEN_STATUS)
                 .orderByDesc(EarlyWarning::getCreateTime)
@@ -170,6 +171,7 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
                         + buildWarningName(warningType != null ? warningType : warningCode, result.getLevel()));
             }
         } else {
+            warning.setDeviceName(deviceCode);
             warning.setLocation("设备编码:" + deviceCode);
         }