Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

林仔 1 неделя назад
Родитель
Сommit
ad59f085df
71 измененных файлов с 3182 добавлено и 131 удалено
  1. 3 0
      .gitignore
  2. 15 0
      AGENTS.md
  3. 10 0
      CONTEXT-MAP.md
  4. 36 0
      docs/agents/domain.md
  5. 19 0
      docs/agents/issue-tracker.md
  6. 11 0
      docs/agents/triage-labels.md
  7. 97 0
      pipe-network-service/CONTEXT.md
  8. 7 0
      pipe-network-service/docs/adr/0001-unify-device-work-under-work-orders.md
  9. 7 0
      pipe-network-service/docs/adr/0002-runtime-owns-device-alarm-state.md
  10. 24 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/WarningThresholdController.java
  11. 31 4
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/BusinessWorkOrderController.java
  12. 13 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/MaintenanceDataStatController.java
  13. 317 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeDeviceController.java
  14. 46 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeLayerController.java
  15. 3 3
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/WorkOrderController.java
  16. 15 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/system/SysLoginController.java
  17. 184 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningController.java
  18. 12 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/WarningThresholdService.java
  19. 66 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/WarningThresholdServiceImpl.java
  20. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/EquipmentMaintain.java
  21. 10 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/WorkOrder.java
  22. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/EquipmentMaintainDataVO.java
  23. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/MaintainOrderVO.java
  24. 111 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillAlarmSyncService.java
  25. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillSimulationService.java
  26. 103 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/impl/DrillSimulationServiceImpl.java
  27. 16 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/BusinessWorkOrderService.java
  28. 22 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/EquipmentBaseService.java
  29. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/MaintenanceDataStatService.java
  30. 27 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/ManholeLayerService.java
  31. 2 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/WorkOrderService.java
  32. 111 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImpl.java
  33. 405 52
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/EquipmentBaseServiceImpl.java
  34. 429 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/MaintenanceDataStatServiceImpl.java
  35. 235 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/ManholeLayerServiceImpl.java
  36. 32 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/MonitoringServiceImpl.java
  37. 7 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImpl.java
  38. 120 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WorkOrderServiceImpl.java
  39. 10 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/EarlyWarning.java
  40. 0 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningArchive.java
  41. 0 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningAttachment.java
  42. 13 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningClearDTO.java
  43. 13 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningConfirmDTO.java
  44. 0 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningItem.java
  45. 13 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningProcessDTO.java
  46. 0 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningReason.java
  47. 39 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningSupervision.java
  48. 0 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/domain/WarningTodo.java
  49. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/EarlyWarningMapper.java
  50. 13 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/WarningSupervisionMapper.java
  51. 15 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/EarlyWarningService.java
  52. 202 10
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningServiceImpl.java
  53. 21 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/AlarmMonitorListInDTO.java
  54. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/MaintenanceDataStatDashboardInDTO.java
  55. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceListInDTO.java
  56. 24 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceStatisticsInDTO.java
  57. 10 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderAddInDTO.java
  58. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderOperInDTO.java
  59. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderPageInDTO.java
  60. 26 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/AlarmMonitorListOutDTO.java
  61. 7 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/EquipmentBaseOutDTO.java
  62. 40 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/MaintenanceDataStatDashboardOutDTO.java
  63. 24 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeDataOutDTO.java
  64. 37 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeLayerStatisticsOutDTO.java
  65. 3 2
      pipe-network-service/zksy-system/src/main/resources/mapper/WorkOrderMapper.xml
  66. 12 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml
  67. 4 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/WarningDisposalMapper.xml
  68. 9 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/WarningSupervisionMapper.xml
  69. 10 0
      zk-api-service/src/main/java/com/zksy/api/domain/EarlyWarning.java
  70. 10 4
      zk-api-service/src/main/java/com/zksy/api/domain/WorkOrder.java
  71. 10 2
      zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

+ 3 - 0
.gitignore

@@ -39,6 +39,7 @@ build/
 .DS_Store
 
 logs/
+/.local/
 
 /pipe-network-service/供水监测报警接口冒烟测试.ps1
 /pipe-network-service/供水监测报警模块测试方案.md
@@ -49,3 +50,5 @@ logs/
 /findings.md
 /docs/superpowers/plans/2026-08-13-water-supply-statistics-gis.md
 /docs/superpowers/specs/2026-08-13-water-supply-statistics-gis-design.md
+/pipe-network-service/sql/water_supply_menu.sql
+/pipe-network-service/sql/water_supply_route_alignment.sql

+ 15 - 0
AGENTS.md

@@ -0,0 +1,15 @@
+# 项目协作规则
+
+## Agent skills
+
+### Issue tracker
+
+Issues are tracked infrequently in the internal Gogs Issues through its web interface. See `docs/agents/issue-tracker.md`.
+
+### Triage labels
+
+Use the five default triage labels. See `docs/agents/triage-labels.md`.
+
+### Domain docs
+
+Use the multi-context layout. See `docs/agents/domain.md`.

+ 10 - 0
CONTEXT-MAP.md

@@ -0,0 +1,10 @@
+# Context Map
+
+## Contexts
+
+- [管网业务服务](./pipe-network-service/CONTEXT.md):承载城市生命线中的管网业务;当前已建模供水安全运行监测领域,其他领域按需补充。
+
+## Relationships
+
+- **供水设施与监测设备**:供水设施描述供水系统中的业务对象;监测设备负责采集运行数据,两者不是同一概念。
+- **报警记录、设备报警状态与工单处置**:设备自动报警时生成报警记录并进入报警状态;人工可派发故障维修工单,但设备报警状态由运行系统根据有效报警原因自动维护。

+ 36 - 0
docs/agents/domain.md

@@ -0,0 +1,36 @@
+# Domain Docs
+
+This repository uses a multi-context domain documentation layout.
+
+## Before exploring
+
+Read the root `CONTEXT-MAP.md`, then read the `CONTEXT.md` files for relevant
+Maven modules. Read system-wide ADRs under `docs/adr/` and module-specific ADRs
+under `<module>/docs/adr/`.
+
+If these files do not exist, proceed silently. Domain-modeling skills create
+them lazily when terminology or decisions are resolved.
+
+## Layout
+
+```text
+/
+|-- CONTEXT-MAP.md
+|-- docs/adr/
+|-- pipe-network-service/CONTEXT.md
+|-- environment-service/CONTEXT.md
+|-- zk-gateway/CONTEXT.md
+|-- zk-common/CONTEXT.md
+|-- zk-api-service/CONTEXT.md
+|-- manhole-service/CONTEXT.md
+|-- firefighting-pressure-service/CONTEXT.md
+|-- flammable-gas-service/CONTEXT.md
+|-- telemetry-service/CONTEXT.md
+|-- radar-service/CONTEXT.md
+|-- audio-service/CONTEXT.md
+`-- water-level-service/CONTEXT.md
+```
+
+Each module may store scoped decisions under `<module>/docs/adr/`. Use the
+terminology defined by the relevant context consistently. Explicitly flag any
+proposal that conflicts with an existing ADR.

+ 19 - 0
docs/agents/issue-tracker.md

@@ -0,0 +1,19 @@
+# Issue tracker: Gogs
+
+Issues and specs for this repository live in the internal Gogs Issues:
+
+- `http://192.168.110.30:3000/shaoy/pipe-ner-server/issues`
+
+## Workflow
+
+Issue tracking is used infrequently in the current project workflow. Create, read,
+comment on, label, and close issues manually through the Gogs web interface.
+No CLI or API integration is assumed.
+
+When a skill says to publish to the issue tracker, prepare the title, body, and
+labels, then create the issue through Gogs. When a skill says to fetch a ticket,
+open the referenced Gogs issue and read its description, labels, and comments.
+
+## Pull requests as a triage surface
+
+Pull requests are not a request or triage surface.

+ 11 - 0
docs/agents/triage-labels.md

@@ -0,0 +1,11 @@
+# Triage Labels
+
+| Canonical role    | Gogs label        | Meaning                                |
+| ----------------- | ----------------- | -------------------------------------- |
+| `needs-triage`    | `needs-triage`    | Maintainer needs to evaluate the issue |
+| `needs-info`      | `needs-info`      | Waiting for more information           |
+| `ready-for-agent` | `ready-for-agent` | Fully specified and ready for an agent |
+| `ready-for-human` | `ready-for-human` | Requires human implementation          |
+| `wontfix`         | `wontfix`         | Will not be actioned                   |
+
+When a skill names a canonical role, use the corresponding Gogs label.

+ 97 - 0
pipe-network-service/CONTEXT.md

@@ -0,0 +1,97 @@
+# 管网业务服务
+
+该上下文承载城市生命线中的管网业务。当前词汇表聚焦供水安全运行监测,覆盖供水设施、监测设备、监测报警和工单处置。
+
+## 供水设施
+
+**供水设施**:
+构成供水系统并被登记、查询、统计或在地图上展示的业务对象,包括水源地、水厂、泵站、供水管线和用水户。
+_Avoid_:把监测设备统称为供水设施
+
+**水源地**:
+为供水系统提供原水的来源地。
+_Avoid_:水源、水源点
+
+**水厂**:
+对原水进行处理并形成可供给用水户水量的生产设施。
+_Avoid_:供水厂、制水厂
+
+**泵站**:
+通过水泵为供水过程提供输送或增压能力的设施。
+_Avoid_:加压站
+
+**供水管线**:
+连接供水设施并输送水的线性设施。
+_Avoid_:管网、管道(指单条设施时)
+
+**用水户**:
+接受供水服务的居民、工业、商业或行政事业主体。
+_Avoid_:大用水户(除非明确指满足特定用水量或重要性标准的子集)、用户
+
+## 运行监测
+
+**监测设备**:
+用于采集供水运行数据的设备,例如水位、压力或流量监测设备;它与被监测的供水设施是不同业务对象。
+_Avoid_:供水设施、监测点(指设备本身时)
+
+**设备异常**:
+监测设备当前处于报警或维修状态的统称,可由一个或多个报警记录或处置过程造成。
+_Avoid_:报警记录(指一次事件时)、故障(原因尚未确认时)
+
+**设备报警状态**:
+表示监测设备当前是否处于报警中的设备级状态;`0` 表示正常,`1` 表示报警,是判断设备当前是否报警的主要依据,由运行系统根据设备的有效报警原因自动维护。
+_Avoid_:报警记录状态、工单状态
+
+**有效报警原因**:
+当前仍满足报警条件、尚未自行恢复的设备异常原因;同一设备可以同时存在多个有效报警原因。
+_Avoid_:报警记录、未结工单
+
+**报警记录**:
+设备因阈值越界或其他自动报警原因而产生的一次事件记录,用于保留报警事实和历史,不作为设备当前是否报警的主要依据。
+_Avoid_:设备报警状态、设备异常
+
+**报警记录状态**:
+表示单条报警记录是否已经人工处置的辅助状态;它不决定设备当前是否处于报警中。
+_Avoid_:设备报警状态
+
+**报警恢复**:
+某个有效报警原因不再满足报警条件的结果,可由实时监测数据、上游恢复事件或有权限人员的人工确认产生。
+_Avoid_:报警记录已处理、工单结案
+
+**清警复核**:
+汇总检查设备是否仍有有效报警原因的活动;报警恢复或工单结案可以触发复核,但只有不存在有效报警原因时才允许清警。
+_Avoid_:直接清警、工单验收
+
+**自动清警**:
+运行系统确认设备的所有有效报警原因均已消除后,将设备报警状态从报警恢复为正常;任一报警原因仍有效时不得清警。
+_Avoid_:工单结案、报警记录已处理
+
+## 工单处置
+
+**工单**:
+对设备故障、日常巡检或设备保养进行分派、执行和跟踪的统一处置载体。
+_Avoid_:维修任务、报警工单(作为独立于工单的概念)
+
+**故障维修工单**:
+用于核实并修复设备故障的工单,工单类型为 `1`;由设备维修需求或监测报警转入的处置都属于此类型。
+_Avoid_:维修任务、报警工单
+
+**工单派发**:
+用户针对报警或异常设备创建故障维修工单并指定后续处置责任人的活动;可从设备维修派单、设备异常管理或报警管理入口发起。
+_Avoid_:报警处理、报警审核
+
+**工单处置**:
+被派单人员对工单执行接收、处理、提交验收和结案等后续活动;工单进度不直接决定设备报警状态,也不阻止设备满足恢复条件时自动清警。
+_Avoid_:报警审核(指工单后续操作时)、报警记录已处理
+
+**工单验收**:
+对工单处置结果是否满足要求进行确认的活动;验收通过可以使工单结案,但不直接清除设备报警状态。
+_Avoid_:报警审核、自动清警
+
+**日常巡检工单**:
+用于执行计划内或临时巡查的工单,工单类型为 `2`。
+_Avoid_:巡检任务
+
+**设备保养工单**:
+用于执行设备预防性保养的工单,工单类型为 `3`。
+_Avoid_:保养任务、维修工单

+ 7 - 0
pipe-network-service/docs/adr/0001-unify-device-work-under-work-orders.md

@@ -0,0 +1,7 @@
+---
+status: accepted
+---
+
+# Unify device work under work orders
+
+Use the work order as the single lifecycle for device fault repair, routine inspection, and preventive maintenance, distinguished by order types `1`, `2`, and `3`. Device repair requests and alarm-driven dispatches are both fault repair work orders (`1`); they must not evolve as separate maintenance-task or alarm-order lifecycles because parallel workflows produce inconsistent assignment, processing, and acceptance states.

+ 7 - 0
pipe-network-service/docs/adr/0002-runtime-owns-device-alarm-state.md

@@ -0,0 +1,7 @@
+---
+status: accepted
+---
+
+# Runtime owns device alarm state
+
+The runtime monitoring system is the authority for a device's alarm state: it sets the device to alarming while any effective alarm cause remains and clears it only after all causes recover through monitoring data, an upstream recovery event, or an authorized manual confirmation. Work order completion may trigger a fresh evaluation but must never clear the device unconditionally, and an alarm record's handled state does not determine the device's current alarm state.

+ 24 - 2
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/WarningThresholdController.java

@@ -36,6 +36,18 @@ public class WarningThresholdController {
                                  @ApiParam(value = "预警编码", required = false) String warningCode){
         return AjaxResult.success(service.findByPage(pageNum, pageSize, deviceCode,warningType,warningCode));
     }
+
+    @GetMapping("/findByModulePage")
+    @ApiOperation(value = "按设备模块(燃气/排水/供水)分页查询该模块所有设备的预警阈值")
+    @Anonymous
+    public AjaxResult findByModulePage(@ApiParam(value = "页码", required = true)long pageNum,
+                                       @ApiParam(value = "页数", required = true)long pageSize,
+                                       @ApiParam(value = "设备模块类型名称,如:燃气、排水、供水", required = true) String typeName,
+                                       @ApiParam(value = "设备编码,模块内检索单个设备", required = false)String deviceCode,
+                                       @ApiParam(value = "预警类型", allowableValues = "温度预警,压力预警,湿度预警", required = false) String warningType,
+                                       @ApiParam(value = "预警编码", required = false) String warningCode){
+        return AjaxResult.success(service.findByModulePage(pageNum, pageSize, typeName, deviceCode, warningType, warningCode));
+    }
     @GetMapping("/getWarningThresholdList")
     @ApiOperation(value = "预警阈值信息查询")
     @Anonymous
@@ -52,12 +64,22 @@ public class WarningThresholdController {
     }
     @PostMapping("/save")
     @ApiOperation(value = "预警阈值信息保存")
-    public AjaxResult save(@RequestBody WarningThreshold entity) {
+    public AjaxResult save(@RequestBody WarningThreshold entity,
+                           @RequestParam(required = false) String moduleType) {
+        if (org.springframework.util.StringUtils.hasText(moduleType)
+                && !service.isDeviceInModule(entity.getDeviceCode(), moduleType)) {
+            return AjaxResult.error("设备不属于指定模块,无法保存阈值");
+        }
         return service.save(entity) ? AjaxResult.success(entity): AjaxResult.error("保存失败");
     }
     @PostMapping("/update")
     @ApiOperation(value = "预警阈值信息修改")
-    public AjaxResult update(@RequestBody WarningThreshold entity) {
+    public AjaxResult update(@RequestBody WarningThreshold entity,
+                             @RequestParam(required = false) String moduleType) {
+        if (org.springframework.util.StringUtils.hasText(moduleType)
+                && !service.isDeviceInModule(entity.getDeviceCode(), moduleType)) {
+            return AjaxResult.error("设备不属于指定模块,无法保存阈值");
+        }
         entity.setUpdateTime(LocalDateTime.now());
         return service.updateById(entity) ? AjaxResult.success(entity): AjaxResult.error("修改失败");
     }

+ 31 - 4
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/BusinessWorkOrderController.java

@@ -31,7 +31,7 @@ public class BusinessWorkOrderController extends BaseController {
 
     @GetMapping("/list")
     @ApiOperation("查询工单列表(equipmentType:gas-燃气 drainage-排水,为空默认燃气)")
-    public AjaxResult list(Integer orderType, Integer orderLevel, Integer orderStatus, String deviceCode, String keyword, String equipmentType, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) {
+    public AjaxResult list(Integer orderType, Integer orderLevel, String orderStatus, String deviceCode, String keyword, String equipmentType, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) {
 
         IPage<WorkOrder> page = businessWorkOrderService.selectWorkOrderList(orderType, orderLevel, orderStatus, deviceCode, keyword, equipmentType, pageNum, pageSize);
 
@@ -43,6 +43,16 @@ public class BusinessWorkOrderController extends BaseController {
         return AjaxResult.success(data);
     }
 
+    @PostMapping("/add")
+    @ApiOperation("手动新增工单(故障维修/日常巡检/设备保养)")
+    public AjaxResult add(@RequestBody WorkOrder workOrder) {
+        if (workOrder.getOrderDesc() == null || workOrder.getOrderDesc().isEmpty()) {
+            return AjaxResult.error("工单描述不能为空");
+        }
+        Long orderId = businessWorkOrderService.createWorkOrder(workOrder);
+        return AjaxResult.success("工单创建成功", orderId);
+    }
+
     @GetMapping("/{orderId}")
     @ApiOperation("查询工单详情(含操作日志)")
     public AjaxResult detail(@PathVariable Long orderId) {
@@ -108,20 +118,25 @@ public class BusinessWorkOrderController extends BaseController {
         Long orderId = Long.valueOf(body.get("orderId").toString());
         String handleResult = body.get("handleResult") != null ? body.get("handleResult").toString() : "";
         String solution = body.get("solution") != null ? body.get("solution").toString() : "";
+        String maintenanceBeforeImg = body.get("maintenanceBeforeImg") != null ? body.get("maintenanceBeforeImg").toString() : "";
         Long userId = SecurityUtils.getUserId();
         String userName = SecurityUtils.getUsername();
 
-        businessWorkOrderService.processOrder(orderId, handleResult, solution, userId, userName);
+        businessWorkOrderService.processOrder(orderId, handleResult, solution, maintenanceBeforeImg, userId, userName);
         return AjaxResult.success("处理完成");
     }
 
     @PutMapping("/complete")
     @ApiOperation("完成/提交验收")
-    public AjaxResult complete(@RequestParam Long orderId, @RequestParam String result) {
+    public AjaxResult complete(@RequestBody Map<String, Object> body) {
+        Long orderId = Long.valueOf(body.get("orderId").toString());
+        String result = body.get("result") != null ? body.get("result").toString() : "";
+        String maintenanceAfterImg = body.get("maintenanceAfterImg") != null
+                ? body.get("maintenanceAfterImg").toString() : "";
         Long userId = SecurityUtils.getUserId();
         String userName = SecurityUtils.getUsername();
 
-        businessWorkOrderService.completeOrder(orderId, result, userId, userName);
+        businessWorkOrderService.completeOrder(orderId, result, maintenanceAfterImg, userId, userName);
         return AjaxResult.success("提交验收成功");
     }
 
@@ -160,6 +175,18 @@ public class BusinessWorkOrderController extends BaseController {
         return AjaxResult.success("工单创建成功", orderId);
     }
 
+    @PutMapping("/reject")
+    @ApiOperation("驳回工单(待派单/待接单 -> 已驳回)")
+    public AjaxResult reject(@RequestBody Map<String, Object> body) {
+        Long orderId = Long.valueOf(body.get("orderId").toString());
+        String reason = body.get("reason") != null ? body.get("reason").toString() : "";
+        Long userId = SecurityUtils.getUserId();
+        String userName = SecurityUtils.getUsername();
+
+        businessWorkOrderService.rejectOrder(orderId, reason, userId, userName);
+        return AjaxResult.success("驳回成功");
+    }
+
     @DeleteMapping("/{orderId}")
     @ApiOperation("取消工单")
     public AjaxResult cancel(@PathVariable Long orderId) {

+ 13 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/MaintenanceDataStatController.java

@@ -5,10 +5,12 @@ import com.zksy.base.service.MaintenanceDataStatService;
 import com.zksy.common.annotation.Anonymous;
 import com.zksy.common.core.controller.BaseController;
 import com.zksy.common.core.domain.AjaxResult2;
+import com.zksy.manhole.dto.in.MaintenanceDataStatDashboardInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatQueryInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatTrendInDTO;
 import com.zksy.manhole.dto.out.AbnormalTerminalTrendOutDTO;
 import com.zksy.manhole.dto.out.CaseRateTrendOutDTO;
+import com.zksy.manhole.dto.out.MaintenanceDataStatDashboardOutDTO;
 import com.zksy.manhole.dto.out.MaintenanceDataStatOutDTO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -70,4 +72,15 @@ public class MaintenanceDataStatController extends BaseController
     {
         return AjaxResult2.success(maintenanceDataStatService.caseRateTrend(inDTO));
     }
+
+    /**
+     * 运维看板统计数据:一次返回KPI指标、异常终端趋势、处置率/延期率趋势、问题类型分布、区域排行、异常终端列表
+     */
+    @Anonymous
+    @PostMapping(value = "/getDashboardStat")
+    @ApiOperation(value = "运维看板统计数据")
+    public AjaxResult2<MaintenanceDataStatDashboardOutDTO> getDashboardStat(@RequestBody MaintenanceDataStatDashboardInDTO inDTO)
+    {
+        return AjaxResult2.success(maintenanceDataStatService.getDashboardStat(inDTO));
+    }
 }

+ 317 - 2
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeDeviceController.java

@@ -10,8 +10,15 @@ import com.zksy.base.alarm.service.WarningThresholdService;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.EquipmentStatus;
 import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.ManholeDeviceRel;
+import com.zksy.base.environment.domain.ERealTimeData;
+import com.zksy.base.environment.mapper.ERealTimeDataMapper;
 import com.zksy.base.manhole.domain.ManholeData;
+import com.zksy.base.radar.domain.RadarData;
+import com.zksy.base.radar.mapper.RadarDataMapper;
 import com.zksy.base.manhole.service.ManholeDataService;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.ManholeDeviceRelMapper;
 import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.base.service.EquipmentStatusService;
 import com.zksy.base.service.EquipmentTypeService;
@@ -30,7 +37,11 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 窨井盖监测系统-监测设备信息管理Controller
@@ -58,6 +69,18 @@ public class ManholeDeviceController extends BaseController
     @Autowired
     private WarningThresholdService warningThresholdService;
 
+    @Autowired
+    private ManholeDeviceRelMapper manholeDeviceRelMapper;
+
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private ERealTimeDataMapper eRealTimeDataMapper;
+
+    @Autowired
+    private RadarDataMapper radarDataMapper;
+
     /**
      * 分页查询监测设备台账列表
      */
@@ -70,16 +93,50 @@ public class ManholeDeviceController extends BaseController
     }
 
     /**
-     * 查询井盖监测设备列表
+     * 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
+     */
+    @Anonymous
+    @PostMapping(value = "/statistics")
+    @ApiOperation(value = "窨井设备统计(区域分布/类型分布)")
+    public AjaxResult2<Map<String, Object>> statistics(@RequestBody ManholeDeviceStatisticsInDTO inDTO)
+    {
+        return AjaxResult2.success(equipmentBaseService.getManholeDeviceStatistics(inDTO));
+    }
+
+    /**
+     * 【态势图层专用】查询井盖监测设备列表(返回全量设备,前端按状态筛选)
+     * 【重要注释】本接口仅查询窨井类型设备:
+     *   - 排水窨井 (equipment_type_id = 9)
+     *   - 供水窨井 (equipment_type_id = 10)
+     * 不包含窨井下面的子设备,仅查询顶级窨井设备
+     * 【报警级别映射】alarmLevel: 0-有报警,1-严重报警,2-一般报警,不传则返回全部
+     * 【告警判断】通过 alarm_data 表 alarm_level 字段:
+     *   - alarm_level = 1 → 严重报警
+     *   - alarm_level > 1 → 一般报警
+     * 【查询内容】运维状态(currentStatus)、报警状态(alarmStatus)、在线状态(onlineStatus)均从设备状态表关联查询
      */
     @Anonymous
     @PostMapping(value = "/getManholeDataList")
     @ApiOperation(value = "查询井盖监测设备列表")
-    public AjaxResult2<List<EquipmentBaseOutDTO>> getManholeDataList(@RequestBody ManholeDeviceListInDTO listInDTO)
+    public AjaxResult2<Page<EquipmentBaseOutDTO>> getManholeDataList(@RequestBody ManholeDeviceListInDTO listInDTO)
     {
         return AjaxResult2.success(equipmentBaseService.getManholeDataList(listInDTO));
     }
 
+    /**
+     * 窨井盖实时监测-设备报警监控列表
+     * 查询所有窨井类型设备(排水=9/供水=10)的 jg_device_data 最新一条记录
+     * 根据倾斜角度(angle_alarm_threshold)、水浸(water_infiltration_alarm_status)、水位(water_level_alarm_status)判定严重告警
+     * 低电量(battery<=20)/弱信号(signal<=20)判定一般告警
+     */
+    @Anonymous
+    @PostMapping(value = "/getAlarmMonitorList")
+    @ApiOperation(value = "窨井盖实时监测-设备报警监控列表")
+    public AjaxResult2<AlarmMonitorListOutDTO> getAlarmMonitorList(@RequestBody AlarmMonitorListInDTO inDTO)
+    {
+        return AjaxResult2.success(equipmentBaseService.getAlarmMonitorList(inDTO));
+    }
+
     /**
      * 获取监测设备台账详情
      */
@@ -158,6 +215,264 @@ public class ManholeDeviceController extends BaseController
                 : AjaxResult.error("保存失败",result);
     }
 
+    /**
+     * 搜索可关联设备(排除窨井类型5/9/10,排除已关联设备ID)
+     */
+    @Anonymous
+    @GetMapping(value = "/rel/search")
+    @ApiOperation(value = "搜索可关联设备")
+    public AjaxResult2<List<Map<String, Object>>> searchRelDevices(
+            @RequestParam(value = "keyword", required = false) String keyword,
+            @RequestParam(value = "excludeIds", required = false) String excludeIds)
+    {
+        LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
+        // 排除窨井类型设备(5=供水窨井, 9=排水窨井, 10=燃气窨井)
+        queryWrapper.notIn(EquipmentBase::getEquipmentTypeId, new String[]{"5", "9", "10"});
+        if (StringUtils.isNotBlank(keyword)) {
+            queryWrapper.and(w -> w
+                    .like(EquipmentBase::getEquipmentCode, keyword)
+                    .or()
+                    .like(EquipmentBase::getEquipmentName, keyword)
+                    .or()
+                    .like(EquipmentBase::getEquipmentLocation, keyword));
+        }
+        // 排除已关联的设备ID
+        if (StringUtils.isNotEmpty(excludeIds)) {
+            List<String> idList = java.util.Arrays.asList(excludeIds.split(","));
+            queryWrapper.notIn(EquipmentBase::getEquipmentId, idList);
+        }
+        queryWrapper.last("LIMIT 50");
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectList(queryWrapper);
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (EquipmentBase eq : equipmentList) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("equipmentId", eq.getEquipmentId());
+            item.put("equipmentCode", eq.getEquipmentCode());
+            item.put("equipmentName", eq.getEquipmentName());
+            item.put("equipmentLocation", eq.getEquipmentLocation());
+            item.put("equipmentTypeId", eq.getEquipmentTypeId());
+            result.add(item);
+        }
+        return AjaxResult2.success(result);
+    }
+
+    /**
+     * 查询设备已关联的井下设备列表
+     */
+    @Anonymous
+    @GetMapping(value = "/rel/list/{equipmentId}")
+    @ApiOperation(value = "查询设备已关联的井下设备列表")
+    public AjaxResult2<List<Map<String, Object>>> getRelList(@PathVariable("equipmentId") String equipmentId)
+    {
+        // 查询关联记录(manholeId 存储主设备ID)
+        List<ManholeDeviceRel> relList = manholeDeviceRelMapper.selectList(
+                new LambdaQueryWrapper<ManholeDeviceRel>()
+                        .eq(ManholeDeviceRel::getManholeId, equipmentId));
+        if (CollUtil.isEmpty(relList)) {
+            return AjaxResult2.success(new ArrayList<>());
+        }
+        // 批量查询关联设备详情
+        List<String> relEquipmentIds = relList.stream()
+                .map(ManholeDeviceRel::getEquipmentId)
+                .collect(Collectors.toList());
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectBatchIds(relEquipmentIds);
+        Map<String, EquipmentBase> equipmentMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentList)) {
+            equipmentMap = equipmentList.stream()
+                    .collect(Collectors.toMap(EquipmentBase::getEquipmentId, e -> e, (a, b) -> a));
+        }
+        // 组装结果
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ManholeDeviceRel rel : relList) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("relId", rel.getRelId());
+            item.put("equipmentId", rel.getEquipmentId());
+            EquipmentBase eq = equipmentMap.get(rel.getEquipmentId());
+            if (eq != null) {
+                item.put("equipmentCode", eq.getEquipmentCode());
+                item.put("equipmentName", eq.getEquipmentName());
+                item.put("equipmentLocation", eq.getEquipmentLocation());
+                item.put("equipmentTypeId", eq.getEquipmentTypeId());
+            }
+            item.put("createTime", rel.getCreateTime());
+            result.add(item);
+        }
+        return AjaxResult2.success(result);
+    }
+
+    /**
+     * 新增窨井井下设备关联
+     */
+    @Anonymous
+    @PostMapping(value = "/rel/save")
+    @ApiOperation(value = "新增窨井井下设备关联")
+    public AjaxResult saveRel(@RequestBody Map<String, String> params)
+    {
+        String manholeId = params.get("manholeId");
+        String equipmentId = params.get("equipmentId");
+        if (StringUtils.isEmpty(manholeId) || StringUtils.isEmpty(equipmentId)) {
+            return AjaxResult.error("主设备ID和关联设备ID不能为空");
+        }
+        if (manholeId.equals(equipmentId)) {
+            return AjaxResult.error("不能关联自身");
+        }
+        // 检查是否已存在关联
+        Long count = manholeDeviceRelMapper.selectCount(
+                new LambdaQueryWrapper<ManholeDeviceRel>()
+                        .eq(ManholeDeviceRel::getManholeId, manholeId)
+                        .eq(ManholeDeviceRel::getEquipmentId, equipmentId));
+        if (count > 0) {
+            return AjaxResult.error("该设备已关联,请勿重复关联");
+        }
+        // 查询设备名称和类型(冗余存储)
+        EquipmentBase equipment = equipmentBaseMapper.selectById(equipmentId);
+        ManholeDeviceRel rel = new ManholeDeviceRel();
+        rel.setManholeId(manholeId);
+        rel.setEquipmentId(equipmentId);
+        if (equipment != null) {
+            rel.setEquipmentName(equipment.getEquipmentName());
+            rel.setEquipmentType(equipment.getEquipmentTypeId());
+        }
+        rel.setCreateTime(LocalDateTime.now());
+        boolean result = manholeDeviceRelMapper.insert(rel) > 0;
+        return result ? AjaxResult.success("关联成功", result)
+                : AjaxResult.error("关联失败", result);
+    }
+
+    /**
+     * 删除窨井井下设备关联
+     */
+    @Anonymous
+    @DeleteMapping(value = "/rel/delete")
+    @ApiOperation(value = "删除窨井井下设备关联")
+    public AjaxResult deleteRel(@RequestParam("relId") Long relId)
+    {
+        if (relId == null) {
+            return AjaxResult.error("关联ID不能为空");
+        }
+        boolean result = manholeDeviceRelMapper.deleteById(relId) > 0;
+        return result ? AjaxResult.success("取消关联成功", result)
+                : AjaxResult.error("取消关联失败", result);
+    }
+
+    /**
+     * 查询设备关联的井下设备最新监测数据
+     * 根据关联设备类型查询 e_real_time_data / radar_data 等数据表
+     */
+    @Anonymous
+    @GetMapping(value = "/rel/data/{equipmentId}")
+    @ApiOperation(value = "查询设备关联的井下设备最新监测数据")
+    public AjaxResult2<List<Map<String, Object>>> getRelDeviceData(@PathVariable("equipmentId") String equipmentId)
+    {
+        // 1. 查询关联设备记录
+        List<ManholeDeviceRel> relList = manholeDeviceRelMapper.selectList(
+                new LambdaQueryWrapper<ManholeDeviceRel>()
+                        .eq(ManholeDeviceRel::getManholeId, equipmentId));
+        if (CollUtil.isEmpty(relList)) {
+            return AjaxResult2.success(new ArrayList<>());
+        }
+        // 2. 批量查询关联设备基础信息
+        List<String> relEquipmentIds = relList.stream()
+                .map(ManholeDeviceRel::getEquipmentId)
+                .collect(Collectors.toList());
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectBatchIds(relEquipmentIds);
+        Map<String, EquipmentBase> equipmentMap = CollUtil.isNotEmpty(equipmentList)
+                ? equipmentList.stream().collect(Collectors.toMap(EquipmentBase::getEquipmentId, e -> e, (a, b) -> a))
+                : new HashMap<>();
+        // 3. 批量查询设备类型名称
+        List<String> typeIds = equipmentList.stream()
+                .map(EquipmentBase::getEquipmentTypeId)
+                .filter(StringUtils::isNotEmpty)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<String, EquipmentType> typeMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(typeIds)) {
+            List<EquipmentType> typeList = equipmentTypeService.list(
+                    new LambdaQueryWrapper<EquipmentType>().in(EquipmentType::getId, typeIds));
+            if (CollUtil.isNotEmpty(typeList)) {
+                typeMap = typeList.stream().collect(Collectors.toMap(EquipmentType::getId, t -> t, (a, b) -> b));
+            }
+        }
+        // 4. 逐个查询最新监测数据(按设备类型路由)
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ManholeDeviceRel rel : relList) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("relId", rel.getRelId());
+            item.put("equipmentId", rel.getEquipmentId());
+            item.put("equipmentName", rel.getEquipmentName());
+            item.put("equipmentType", rel.getEquipmentType());
+            EquipmentBase eq = equipmentMap.get(rel.getEquipmentId());
+            if (eq != null) {
+                item.put("equipmentCode", eq.getEquipmentCode());
+                item.put("equipmentLocation", eq.getEquipmentLocation());
+                item.put("equipmentTypeId", eq.getEquipmentTypeId());
+                EquipmentType type = typeMap.get(eq.getEquipmentTypeId());
+                String typeName = type != null ? type.getTypeName() : "";
+                item.put("equipmentTypeName", typeName);
+                // 查询最新监测数据
+                Map<String, Object> monitorData = getLatestMonitorData(eq.getEquipmentCode(), typeName);
+                if (!monitorData.isEmpty()) {
+                    item.put("monitorData", monitorData);
+                }
+            }
+            result.add(item);
+        }
+        return AjaxResult2.success(result);
+    }
+
+    /**
+     * 根据设备类型查询最新监测数据(雷达流量计/环境监测等)
+     */
+    private Map<String, Object> getLatestMonitorData(String deviceCode, String typeName) {
+        Map<String, Object> result = new HashMap<>();
+        if (StringUtils.isEmpty(deviceCode) || StringUtils.isEmpty(typeName)) {
+            return result;
+        }
+        try {
+            if (typeName.contains("雷达") || typeName.contains("流量")) {
+                RadarData data = radarDataMapper.selectOne(
+                        new LambdaQueryWrapper<RadarData>()
+                                .eq(RadarData::getDeviceCode, deviceCode)
+                                .orderByDesc(RadarData::getCreateTime)
+                                .last("LIMIT 1"));
+                if (data != null) {
+                    result.put("instantFlow", data.getMeter1InstantFlow());
+                    result.put("flowSpeed", data.getFlowSpeed());
+                    result.put("waterLevel", data.getWaterLevel());
+                    result.put("totalFlow", data.getMeter1NetTotal());
+                    result.put("signalQuality", data.getSignalQuality());
+                    result.put("createTime", data.getCreateTime());
+                    result.put("dataType", "雷达流量计");
+                }
+            } else if (typeName.contains("环境") || typeName.contains("水质")) {
+                Integer devId = Integer.parseInt(deviceCode);
+                ERealTimeData data = eRealTimeDataMapper.selectOne(
+                        new LambdaQueryWrapper<ERealTimeData>()
+                                .eq(ERealTimeData::getDeviceId, devId)
+                                .orderByDesc(ERealTimeData::getCreateTime)
+                                .last("LIMIT 1"));
+                if (data != null) {
+                    result.put("temperature", data.getTem());
+                    result.put("humidity", data.getHum());
+                    result.put("createTime", data.getCreateTime());
+                    result.put("dataType", "环境监测");
+                    if (data.getNodeId() != null) {
+                        switch (data.getNodeId()) {
+                            case 1: result.put("metricName", "悬浮物"); result.put("metricValue", data.getFloatValue()); break;
+                            case 2: result.put("metricName", "COD"); result.put("metricValue", data.getHum()); break;
+                            case 3: result.put("metricName", "氨氮"); result.put("metricValue", data.getHum()); break;
+                            case 4: result.put("metricName", "电导率"); result.put("metricValue", data.getHum()); break;
+                            case 5: result.put("metricName", "PH"); result.put("metricValue", data.getHum()); break;
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // 设备编码非数字或查询异常时忽略
+        }
+        return result;
+    }
+
     /**
      * 根据设备类别分组统计设备数量
      */

+ 46 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeLayerController.java

@@ -0,0 +1,46 @@
+package com.zksy.web.controller.manhole;
+
+
+import com.zksy.base.service.ManholeLayerService;
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.core.controller.BaseController;
+import com.zksy.common.core.domain.AjaxResult2;
+import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 窨井盖监测系统 - 态势图层 Controller
+ * 
+ * @author xianggx
+ */
+@RestController
+@RequestMapping("/manhole/layer")
+@Api(tags = "窨井盖监测系统 - 态势图层")
+public class ManholeLayerController extends BaseController {
+
+    @Autowired
+    private ManholeLayerService manholeLayerService;
+
+    /**
+     * 获取态势图层数据(设备列表 + 统计)
+     * 【重要说明】本接口仅查窨井类型设备:排水窨井 (equipment_type_id=9)/供水窨井 (equipment_type_id=10)
+     * 不包含窨井下面的子设备,仅查询顶级窨井设备
+     * 【报警级别映射】alarmLevel: 0-有报警,1-严重报警,2-一般报警
+     * 【告警判断】通过 alarm_data 表 alarm_level 字段:
+     *   - alarm_level = 1 → 严重报警
+     *   - alarm_level > 1 → 一般报警
+     * 【查询内容】运维状态、报警状态、在线状态等均从设备状态表关联查询
+     */
+    @Anonymous
+    @PostMapping(value = "/getManholeLayerData")
+    @ApiOperation(value = "获取态势图层数据")
+    public AjaxResult2<Map<String, Object>> getManholeLayerData(@RequestBody ManholeDeviceListInDTO listInDTO) {
+        return AjaxResult2.success(manholeLayerService.getManholeLayerData(listInDTO));
+    }
+
+}

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

@@ -131,9 +131,9 @@ public class WorkOrderController extends BaseController
     @PostMapping(value = "/addWorkOrder")
     @ApiOperation(value = "新增工单")
     public AjaxResult addWorkOrder(@RequestBody WorkOrderAddInDTO inDTO) {
-        boolean result = workOrderService.addWorkOrder(inDTO);
-        return result ? AjaxResult.success("新增成功", result)
-                : AjaxResult.error("新增失败", result);
+        Long orderId = workOrderService.addWorkOrder(inDTO);
+        return orderId != null ? AjaxResult.success("新增成功", orderId)
+                : AjaxResult.error("新增失败");
     }
 
     /**

+ 15 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/system/SysLoginController.java

@@ -76,6 +76,21 @@ public class SysLoginController
         ajax.put(Constants.TOKEN, token);
         return ajax;
     }
+    /**
+     * 退出登录
+     * 
+     * @return 结果
+     */
+    @PostMapping("/logout")
+    public AjaxResult logout()
+    {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (StringUtils.isNotNull(loginUser))
+        {
+            tokenService.delLoginUser(loginUser.getToken());
+        }
+        return AjaxResult.success("退出成功");
+    }
     /**
      * 获取用户信息
      * 

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

@@ -5,8 +5,13 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zksy.base.warning.domain.EarlyWarning;
 import com.zksy.base.warning.domain.WarningArchive;
+import com.zksy.base.warning.domain.WarningAttachment;
 import com.zksy.base.warning.domain.WarningDisposal;
 import com.zksy.base.warning.domain.WarningSaveDTO;
+import com.zksy.base.warning.domain.WarningConfirmDTO;
+import com.zksy.base.warning.domain.WarningProcessDTO;
+import com.zksy.base.warning.domain.WarningClearDTO;
+import com.zksy.base.warning.domain.WarningSupervision;
 import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
@@ -15,13 +20,21 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
+import io.minio.GetObjectArgs;
+import io.minio.MinioClient;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.time.LocalDateTime;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -35,6 +48,12 @@ public class EarlyWarningController {
     @Autowired
     private EarlyWarningService earlyWarningService;
 
+    @Autowired
+    private MinioClient minioClient;
+
+    @Value("${minio.bucket}")
+    private String minioBucket;
+
     @GetMapping("/list")
     @ApiOperation("分页查询预警列表")
     @PreAuthorize("@ss.hasPermi('warning:warning:list')")
@@ -146,6 +165,49 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
         return result ? AjaxResult.success("发布成功") : AjaxResult.error("发布失败");
     }
 
+    @PostMapping("/confirm")
+    @ApiOperation("确认异常并分配处理人")
+    @PreAuthorize("@ss.hasPermi('warning:warning:handle')")
+    public AjaxResult confirmWarning(
+            @RequestParam String warningId,
+            @RequestParam(required = false) String assignedUser,
+            @RequestParam(required = false) String confirmRemark) {
+        boolean result = earlyWarningService.confirmWarning(warningId, assignedUser, confirmRemark);
+        return result ? AjaxResult.success("预警已确认并进入处理") : AjaxResult.error("预警确认失败");
+    }
+
+    @PostMapping("/misreport")
+    @ApiOperation("确认误报并清除预警")
+    @PreAuthorize("@ss.hasPermi('warning:warning:handle')")
+    public AjaxResult misreportWarning(@RequestParam String warningId, @RequestParam String reason) {
+        boolean result = earlyWarningService.misreportWarning(warningId, reason);
+        return result ? AjaxResult.success("误报已清除") : AjaxResult.error("误报处理失败");
+    }
+
+    @PostMapping("/process")
+    @ApiOperation("提交预警处理反馈")
+    @PreAuthorize("@ss.hasPermi('warning:warning:handle')")
+    public AjaxResult submitWarningProcess(
+            @RequestPart("process") String processJson,
+            @RequestPart(value = "files", required = false) List<MultipartFile> files) {
+        try {
+            WarningProcessDTO process = JSON.parseObject(processJson, WarningProcessDTO.class);
+            boolean result = earlyWarningService.submitWarningProcess(process.getWarningId(), process.getProcessContent(), files);
+            return result ? AjaxResult.success("处理反馈已提交") : AjaxResult.error("处理反馈提交失败");
+        } catch (Exception e) {
+            log.error("提交预警处理反馈失败", e);
+            return AjaxResult.error("处理反馈提交失败: " + e.getMessage());
+        }
+    }
+
+    @PostMapping("/clear")
+    @ApiOperation("清除预警")
+    @PreAuthorize("@ss.hasPermi('warning:warning:handle')")
+    public AjaxResult clearWarning(@RequestParam String warningId, @RequestParam String clearRemark) {
+        boolean result = earlyWarningService.clearWarning(warningId, clearRemark);
+        return result ? AjaxResult.success("预警已清除") : AjaxResult.error("预警清除失败");
+    }
+
     @PostMapping("/upgrade")
     @ApiOperation("预警升级")
     @Log(title = "预警升级", businessType = BusinessType.UPDATE)
@@ -262,11 +324,14 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
             @ApiParam("状态") @RequestParam(required = false) String status,
             @ApiParam("预警位置") @RequestParam(required = false) String location,
             @ApiParam("发布人") @RequestParam(required = false) String publisher,
+            @ApiParam("排除发布人(如:系统自动)") @RequestParam(required = false) String excludePublisher,
             @ApiParam("权属单位") @RequestParam(required = false) String ownershipUnit,
             @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
-            @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) {
+            @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @ApiParam("设备编码") @RequestParam(required = false) String deviceCode,
+            @ApiParam("设备名称") @RequestParam(required = false) String deviceName) {
         Page<?> page = new Page<>(pageNum, pageSize);
-        IPage<Map<String, Object>> result = earlyWarningService.searchWarnings(page, warningName, warningNo, warningType, warningLevel, status, location, publisher, ownershipUnit, startTime, endTime);
+        IPage<Map<String, Object>> result = earlyWarningService.searchWarnings(page, warningName, warningNo, warningType, warningLevel, status, location, publisher, ownershipUnit, startTime, endTime, excludePublisher, deviceCode, deviceName);
         return AjaxResult.success(result);
     }
 
@@ -325,6 +390,123 @@ List<String> alarmIds = alarmIdsJson != null && !alarmIdsJson.isEmpty()
         return AjaxResult.success(result);
     }
 
+    @GetMapping("/attachments/{attachmentId}/preview")
+    @ApiOperation("预览预警附件")
+    @PreAuthorize("@ss.hasPermi('warning:warning:query')")
+    public void previewAttachment(@PathVariable String attachmentId, javax.servlet.http.HttpServletResponse response) {
+        streamAttachment(attachmentId, response, false);
+    }
+
+    @GetMapping("/attachments/{attachmentId}/download")
+    @ApiOperation("下载预警附件")
+    @PreAuthorize("@ss.hasPermi('warning:warning:query')")
+    public void downloadAttachment(@PathVariable String attachmentId, javax.servlet.http.HttpServletResponse response) {
+        streamAttachment(attachmentId, response, true);
+    }
+
+    private void streamAttachment(String attachmentId, javax.servlet.http.HttpServletResponse response, boolean download) {
+        WarningAttachment attachment = earlyWarningService.getAttachment(attachmentId);
+        if (attachment == null || attachment.getAttachmentUrl() == null || attachment.getAttachmentUrl().trim().isEmpty()) {
+            sendAttachmentError(response, javax.servlet.http.HttpServletResponse.SC_NOT_FOUND, "附件不存在");
+            return;
+        }
+
+        String objectName = normalizeObjectName(attachment.getAttachmentUrl());
+        if (objectName == null) {
+            sendAttachmentError(response, javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST, "附件地址无效");
+            return;
+        }
+
+        String fileName = displayAttachmentName(attachment.getAttachmentName());
+        try (InputStream input = minioClient.getObject(GetObjectArgs.builder()
+                .bucket(minioBucket)
+                .object(objectName)
+                .build())) {
+            response.reset();
+            response.setContentType(resolveContentType(attachment.getAttachmentType(), fileName));
+            if (attachment.getAttachmentSize() != null && attachment.getAttachmentSize() >= 0) {
+                response.setContentLengthLong(attachment.getAttachmentSize());
+            }
+            response.setHeader("X-Content-Type-Options", "nosniff");
+            response.setHeader("Content-Disposition", (download ? "attachment" : "inline")
+                    + "; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20"));
+            byte[] buffer = new byte[8192];
+            int length;
+            while ((length = input.read(buffer)) != -1) {
+                response.getOutputStream().write(buffer, 0, length);
+            }
+            response.getOutputStream().flush();
+        } catch (Exception e) {
+            log.error("读取预警附件失败,attachmentId={}", attachmentId, e);
+            sendAttachmentError(response, javax.servlet.http.HttpServletResponse.SC_NOT_FOUND, "附件读取失败");
+        }
+    }
+
+    private String normalizeObjectName(String attachmentUrl) {
+        String objectName = attachmentUrl.trim();
+        int bucketIndex = objectName.indexOf("/" + minioBucket + "/");
+        if (objectName.startsWith("http://") || objectName.startsWith("https://")) {
+            try {
+                objectName = new java.net.URI(objectName).getPath();
+            } catch (Exception e) {
+                return null;
+            }
+        }
+        if (bucketIndex >= 0) {
+            objectName = objectName.substring(bucketIndex + minioBucket.length() + 2);
+        } else {
+            objectName = objectName.replaceFirst("^/", "");
+            if (objectName.startsWith(minioBucket + "/")) {
+                objectName = objectName.substring(minioBucket.length() + 1);
+            }
+        }
+        return objectName.isEmpty() || objectName.contains("..") ? null : objectName;
+    }
+
+    private String displayAttachmentName(String attachmentName) {
+        if (attachmentName == null || attachmentName.trim().isEmpty()) {
+            return "预警附件";
+        }
+        return attachmentName.replaceFirst("^\\[(预警报告|处理反馈)\\]\\s*", "");
+    }
+
+    private String resolveContentType(String savedType, String fileName) {
+        String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
+        switch (extension) {
+            case "jpg":
+            case "jpeg": return "image/jpeg";
+            case "png": return "image/png";
+            case "gif": return "image/gif";
+            case "webp": return "image/webp";
+            case "pdf": return "application/pdf";
+            case "xls": return "application/vnd.ms-excel";
+            case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+            case "doc": return "application/msword";
+            case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+            default:
+                return savedType != null && !savedType.trim().isEmpty()
+                        ? savedType : "application/octet-stream";
+        }
+    }
+
+    private void sendAttachmentError(javax.servlet.http.HttpServletResponse response, int status, String message) {
+        try {
+            if (!response.isCommitted()) {
+                response.sendError(status, message);
+            }
+        } catch (IOException ignored) {
+            log.warn("返回附件错误信息失败", ignored);
+        }
+    }
+
+    @GetMapping("/supervision/{warningId}")
+    @ApiOperation("获取预警督办记录")
+    @PreAuthorize("@ss.hasPermi('warning:warning:query')")
+    public AjaxResult getSupervisionList(@PathVariable String warningId) {
+        List<WarningSupervision> result = earlyWarningService.getSupervisionList(warningId);
+        return AjaxResult.success(result);
+    }
+
     @GetMapping("/dashboard/statistics")
     @ApiOperation("获取仪表盘统计数据")
     @PreAuthorize("@ss.hasPermi('warning:warning:query')")

+ 12 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/WarningThresholdService.java

@@ -19,4 +19,16 @@ public interface WarningThresholdService extends IService<WarningThreshold> {
      * 根据设备编号查询该设备所有预警值
      */
     WarningThreshold getWarningThresholdByDeviceAndCode(String deviceCode, String warningCode);
+
+    /**
+     * 按设备模块(燃气/排水/供水)分页查询预警阈值
+     * @param typeName 顶级设备类型名称,如:燃气、排水、供水
+     * @param deviceCode 设备编码,可选,用于在模块内精确检索单个设备
+     */
+    Page<WarningThreshold> findByModulePage(long pageNum, long pageSize, String typeName, String deviceCode, String warningType, String warningCode);
+
+    /**
+     * 校验设备编码是否全部属于指定设备模块。
+     */
+    boolean isDeviceInModule(String deviceCode, String typeName);
 }

+ 66 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/WarningThresholdServiceImpl.java

@@ -7,9 +7,16 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.base.alarm.domain.WarningThreshold;
 import com.zksy.base.alarm.mapper.WarningThresholdMapper;
 import com.zksy.base.alarm.service.WarningThresholdService;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.service.EquipmentBaseService;
+import cn.hutool.core.collection.CollUtil;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
+import java.util.Set;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 
 /**
 * @author Administrator
@@ -20,6 +27,9 @@ import java.util.List;
 public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMapper, WarningThreshold>
     implements WarningThresholdService {
 
+    @Autowired
+    private EquipmentBaseService equipmentBaseService;
+
     @Override
     public Page<WarningThreshold> findByPage(long pageNum, long pageSize, String deviceCode, String warningType, String warningCode) {
         Page<WarningThreshold> page = new Page<>(pageNum, pageSize);
@@ -33,6 +43,62 @@ public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMap
         return this.page(page, queryWrapper);
     }
 
+    @Override
+    public Page<WarningThreshold> findByModulePage(long pageNum, long pageSize, String typeName, String deviceCode, String warningType, String warningCode) {
+        Page<WarningThreshold> page = new Page<>(pageNum, pageSize);
+        // 1. 查询该模块下所有设备编码
+        List<EquipmentBase> equipmentList = equipmentBaseService.findByTopLevelType(typeName);
+        if (CollUtil.isEmpty(equipmentList)) {
+            return page;
+        }
+        List<String> deviceCodes = equipmentList.stream()
+                .map(EquipmentBase::getEquipmentCode)
+                .filter(code -> code != null && !code.isEmpty())
+                .distinct()
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(deviceCodes)) {
+            return page;
+        }
+        // 2. 查询阈值:device_code 字段匹配任意一个模块设备编码(支持逗号分隔多设备)
+        LambdaQueryWrapper<WarningThreshold> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.and(w -> {
+            for (String code : deviceCodes) {
+                w.or().apply("CONCAT(',', device_code, ',') LIKE CONCAT('%,', {0}, ',%')", code);
+            }
+        });
+        // 3. 模块内按设备编码精确检索单个设备
+        if (deviceCode != null && !deviceCode.isEmpty()) {
+            queryWrapper.apply("CONCAT(',', device_code, ',') LIKE CONCAT('%,', {0}, ',%')", deviceCode);
+        }
+        if (warningType != null && !warningType.isEmpty()) {
+            queryWrapper.like(WarningThreshold::getWarningType, warningType);
+        }
+        if (warningCode != null && !warningCode.isEmpty()) {
+            queryWrapper.like(WarningThreshold::getWarningCode, warningCode);
+        }
+        queryWrapper.orderByDesc(WarningThreshold::getUpdateTime);
+        return this.page(page, queryWrapper);
+    }
+
+    @Override
+    public boolean isDeviceInModule(String deviceCode, String typeName) {
+        if (deviceCode == null || deviceCode.trim().isEmpty() || typeName == null || typeName.trim().isEmpty()) {
+            return false;
+        }
+        Set<String> moduleCodes = equipmentBaseService.findByTopLevelType(typeName).stream()
+                .map(EquipmentBase::getEquipmentCode)
+                .filter(code -> code != null && !code.trim().isEmpty())
+                .map(String::trim)
+                .collect(Collectors.toSet());
+        if (moduleCodes.isEmpty()) {
+            return false;
+        }
+        return Arrays.stream(deviceCode.split(","))
+                .map(String::trim)
+                .filter(code -> !code.isEmpty())
+                .allMatch(moduleCodes::contains);
+    }
+
     @Override
     public List<WarningThreshold> getWarningThresholdList(String deviceCode, String warningType, String warningCode) {
         LambdaQueryWrapper<WarningThreshold> queryWrapper = new LambdaQueryWrapper<>();
@@ -60,5 +126,3 @@ public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMap
 }
 
 
-
-

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

@@ -48,7 +48,7 @@ public class EquipmentMaintain {
     /**
      * 维护人联系方式
      */
-    private Integer maintainPersonPhone;
+    private String maintainPersonPhone;
 
     /**
      * 维护日期

+ 10 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/WorkOrder.java

@@ -162,10 +162,16 @@ public class WorkOrder implements Serializable {
     private String repairResult;
 
     /**
-     * 工单图片(多个图片用逗号分隔)
+     * 维修前图片 URL,多个图片使用逗号分隔
      */
-    @ApiModelProperty(value = "工单图片,多个用逗号分隔")
-    private String workImage;
+    @ApiModelProperty(value = "维修前图片 URL")
+    private String maintenanceBeforeImg;
+
+    /**
+     * 维修后图片 URL,多个图片使用逗号分隔
+     */
+    @ApiModelProperty(value = "维修后图片 URL")
+    private String maintenanceAfterImg;
 
     /**
      * 验收人ID
@@ -193,4 +199,4 @@ public class WorkOrder implements Serializable {
     @ApiModelProperty(value = "更新时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date updateTime;
-}
+}

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

@@ -35,7 +35,7 @@ public class EquipmentMaintainDataVO {
     private String maintainPerson;
 
     @ApiModelProperty("维护人联系方式")
-    private Integer maintainPersonPhone;
+    private String maintainPersonPhone;
 
     @ApiModelProperty("维护日期")
     private Date maintainDate;

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

@@ -30,7 +30,7 @@ public class MaintainOrderVO {
     private String maintainPerson;
 
     @ApiModelProperty("维护人电话")
-    private Integer maintainPersonPhone;
+    private String maintainPersonPhone;
 
     @ApiModelProperty("维护费用")
     private Double maintainCost;

+ 111 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillAlarmSyncService.java

@@ -0,0 +1,111 @@
+package com.zksy.base.drill.service;
+
+import cn.hutool.core.date.DateUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.domain.CaseInfo;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.mapper.CaseInfoMapper;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.WorkOrderLogMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * 演练告警同步创建案件/工单 — 从 zk-api-service 的 AlarmDataServiceImpl.syncCreateCaseAndWorkOrder 复制
+ */
+@Slf4j
+@Service("drillAlarmSyncServiceDrill")
+public class DrillAlarmSyncService {
+
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private CaseInfoMapper caseInfoMapper;
+
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
+    @Autowired
+    private WorkOrderLogMapper workOrderLogMapper;
+
+    private static final List<String> CASE_REQUIRED_TYPE_IDS = Arrays.asList("5", "9", "10");
+
+    public void syncCreateCaseAndWorkOrder(AlarmData alarmData) {
+        EquipmentBase equipmentBase = equipmentBaseMapper.selectOne(
+                new LambdaQueryWrapper<EquipmentBase>()
+                        .eq(EquipmentBase::getEquipmentCode, alarmData.getDeviceCode())
+                        .last("LIMIT 1"));
+        if (equipmentBase == null) {
+            log.warn("未找到设备编码对应的设备信息,deviceCode={}", alarmData.getDeviceCode());
+            return;
+        }
+        String typeId = equipmentBase.getEquipmentTypeId();
+        boolean needCreateCase = CASE_REQUIRED_TYPE_IDS.contains(typeId);
+        int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : 4;
+        int orderLevel = caseLevel <= 2 ? 1 : (caseLevel == 3 ? 2 : 3);
+        String desc = alarmData.getWarningType() != null ? alarmData.getWarningType() : "设备告警";
+        if (alarmData.getRemark() != null) {
+            desc += "-" + alarmData.getRemark();
+        }
+        CaseInfo caseInfo = null;
+        if (needCreateCase) {
+            caseInfo = new CaseInfo();
+            caseInfo.setCaseNo(generateCaseNo());
+            caseInfo.setAlarmId(alarmData.getId());
+            caseInfo.setDeviceId(equipmentBase.getEquipmentId());
+            caseInfo.setDeviceCode(alarmData.getDeviceCode());
+            caseInfo.setCaseLevel(caseLevel);
+            caseInfo.setCaseDesc(desc);
+            caseInfo.setCaseSource(1);
+            caseInfo.setCaseStatus(1);
+            caseInfoMapper.insert(caseInfo);
+            log.info("同步新增案件信息成功,caseNo={}", caseInfo.getCaseNo());
+        }
+        WorkOrder workOrder = new WorkOrder();
+        workOrder.setOrderNo(generateOrderNo());
+        workOrder.setAlarmId(alarmData.getId());
+        workOrder.setDeviceId(equipmentBase.getEquipmentId());
+        workOrder.setDeviceCode(alarmData.getDeviceCode());
+        workOrder.setOrderType(1);
+        workOrder.setOrderLevel(orderLevel);
+        workOrder.setOrderDesc(desc);
+        workOrder.setOrderStatus(1);
+        if (caseInfo != null) {
+            workOrder.setCaseId(caseInfo.getCaseId());
+            workOrder.setCaseNo(caseInfo.getCaseNo());
+        }
+        workOrderMapper.insert(workOrder);
+        log.info("同步新增工单成功,orderNo={}", workOrder.getOrderNo());
+        WorkOrderLog workOrderLog = new WorkOrderLog();
+        workOrderLog.setOrderId(workOrder.getOrderId());
+        workOrderLog.setOperType(1);
+        workOrderLog.setOperDesc(desc + ",系统告警自动创建工单");
+        workOrderLog.setOperUserName("系统");
+        workOrderLog.setOperUserId(-1L);
+        workOrderLog.setOperTime(DateUtil.date());
+        workOrderLogMapper.insert(workOrderLog);
+        log.info("同步新增工单操作日志成功,orderId={}", workOrder.getOrderId());
+    }
+
+    private static String generateCaseNo() {
+        String dt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        return "CS" + dt + String.format("%04d", new Random().nextInt(10000));
+    }
+
+    private static String generateOrderNo() {
+        String dt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        return "WO" + dt + String.format("%04d", new Random().nextInt(10000));
+    }
+}

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillSimulationService.java

@@ -0,0 +1,8 @@
+package com.zksy.base.drill.service;
+
+import java.util.Map;
+
+public interface DrillSimulationService {
+
+    Map<String, Object> triggerAlarm(String category);
+}

+ 103 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/impl/DrillSimulationServiceImpl.java

@@ -0,0 +1,103 @@
+package com.zksy.base.drill.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.service.AlarmDataService;
+import com.zksy.base.drill.service.DrillAlarmSyncService;
+import com.zksy.base.drill.service.DrillSimulationService;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+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.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service("drillSimulationServiceImplDrill")
+public class DrillSimulationServiceImpl implements DrillSimulationService {
+
+    @Autowired
+    private EquipmentTypeMapper equipmentTypeMapper;
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+    @Autowired
+    private AlarmDataService alarmDataService;
+    @Autowired
+    private DrillAlarmSyncService drillAlarmSyncService;
+
+    @Override
+    public Map<String, Object> triggerAlarm(String category) {
+        EquipmentType topType = equipmentTypeMapper.selectOne(
+                new LambdaQueryWrapper<EquipmentType>()
+                        .eq(EquipmentType::getTypeName, category)
+                        .eq(EquipmentType::getParentTypeId, "0"));
+        if (topType == null) {
+            throw new IllegalArgumentException("未找到项目类型对应的设备分类:" + category);
+        }
+        List<String> typeIds = new ArrayList<>();
+        typeIds.add(topType.getId());
+        collectChildTypeIds(topType.getId(), typeIds);
+
+        List<EquipmentBase> equipments = equipmentBaseMapper.selectList(
+                new LambdaQueryWrapper<EquipmentBase>()
+                        .in(EquipmentBase::getEquipmentTypeId, typeIds));
+        if (CollUtil.isEmpty(equipments)) {
+            throw new IllegalArgumentException("未找到" + category + "类型下的设备");
+        }
+        Map<String, String> typeNameMap = equipmentTypeMapper.selectBatchIds(typeIds).stream()
+                .collect(Collectors.toMap(EquipmentType::getId, EquipmentType::getTypeName, (a, b) -> a));
+
+        int successCount = 0, failCount = 0;
+        LocalDateTime now = LocalDateTime.now();
+        for (EquipmentBase eq : equipments) {
+            try {
+                AlarmData alarm = new AlarmData();
+                alarm.setDeviceCode(eq.getEquipmentCode());
+                alarm.setWarningType(typeNameMap.getOrDefault(eq.getEquipmentTypeId(), "未知类型") + "模拟告警");
+                alarm.setWarningCode("DRILL-" + System.currentTimeMillis());
+                alarm.setMinValue(new BigDecimal("0"));
+                alarm.setMaxValue(new BigDecimal("100"));
+                alarm.setActualValue(new BigDecimal("150"));
+                alarm.setAlarmLevel(1);
+                alarm.setAlarmStatus(0);
+                alarm.setAlarmTime(now);
+                alarm.setDeviationRatio(new BigDecimal("50"));
+                alarm.setRemark("演练模拟告警-" + category);
+                alarm.setCreateTime(now);
+                boolean saved = alarmDataService.saveAlarmData(alarm);
+                if (saved) {
+                    drillAlarmSyncService.syncCreateCaseAndWorkOrder(alarm);
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                log.error("模拟告警失败, equipmentId={}", eq.getEquipmentId(), e);
+                failCount++;
+            }
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("category", category);
+        result.put("total", equipments.size());
+        result.put("success", successCount);
+        result.put("fail", failCount);
+        return result;
+    }
+
+    private void collectChildTypeIds(String parentId, List<String> typeIds) {
+        List<EquipmentType> children = equipmentTypeMapper.selectList(
+                new LambdaQueryWrapper<EquipmentType>()
+                        .eq(EquipmentType::getParentTypeId, parentId));
+        for (EquipmentType child : children) {
+            typeIds.add(child.getId());
+            collectChildTypeIds(child.getId(), typeIds);
+        }
+    }
+}

+ 16 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/BusinessWorkOrderService.java

@@ -18,7 +18,7 @@ public interface BusinessWorkOrderService extends IService<WorkOrder> {
      * @param equipmentType 设备类型:gas-燃气 drainage-排水,为空查询全部
      */
     IPage<WorkOrder> selectWorkOrderList(Integer orderType, Integer orderLevel,
-                                         Integer orderStatus, String deviceCode,
+                                         String orderStatus, String deviceCode,
                                          String keyword, String equipmentType,
                                          Integer pageNum, Integer pageSize);
 
@@ -50,18 +50,24 @@ public interface BusinessWorkOrderService extends IService<WorkOrder> {
     /**
      * 处理
      */
-    boolean processOrder(Long orderId, String handleResult, String solution, Long operUserId, String operUserName);
+    boolean processOrder(Long orderId, String handleResult, String solution, String maintenanceBeforeImg, Long operUserId, String operUserName);
 
     /**
      * 完成/提交验收
      */
-    boolean completeOrder(Long orderId, String result, Long operUserId, String operUserName);
+    boolean completeOrder(Long orderId, String result, String maintenanceAfterImg, Long operUserId, String operUserName);
 
     /**
      * 取消工单
      */
     boolean cancelOrder(Long orderId, Long operUserId, String operUserName);
 
+    /**
+     * 驳回工单(待派单/待接单 -> 已驳回)
+     * @param reason 驳回原因
+     */
+    boolean rejectOrder(Long orderId, String reason, Long operUserId, String operUserName);
+
     /**
      * 验收通过/结案(status 5→6),同步联动隐患状态为"已整改"
      */
@@ -80,4 +86,11 @@ public interface BusinessWorkOrderService extends IService<WorkOrder> {
      */
     Long createWorkOrderFromHazard(String hazardId, String hazardNo, String hazardDesc, String equipmentId,
                                     String hazardLevel, String hazardImages, Date planFinishTime);
+
+    /**
+     * 手动新增工单(故障维修/日常巡检/设备保养)
+     * @param workOrder 工单信息(orderType/orderLevel/orderDesc/deviceId/deviceCode/planFinishTime)
+     * @return 工单ID
+     */
+    Long createWorkOrder(WorkOrder workOrder);
 }

+ 22 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/EquipmentBaseService.java

@@ -4,10 +4,13 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.vo.EquipmentFullVO;
+import com.zksy.manhole.dto.in.AlarmMonitorListInDTO;
 import com.zksy.manhole.dto.in.EquipmentCountByTypeInDTO;
 import com.zksy.manhole.dto.in.EquipmentExportInDTO;
 import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
 import com.zksy.manhole.dto.in.ManholeDevicePageInDTO;
+import com.zksy.manhole.dto.in.ManholeDeviceStatisticsInDTO;
+import com.zksy.manhole.dto.out.AlarmMonitorListOutDTO;
 import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
 import com.zksy.manhole.dto.out.EquipmentCountByTypeOutDTO;
 import com.zksy.manhole.dto.out.EquipmentExportOutDTO;
@@ -53,11 +56,18 @@ public interface EquipmentBaseService extends IService<EquipmentBase> {
     Page<EquipmentBaseOutDTO> findManholeDeviceByPage(ManholeDevicePageInDTO pageInDTO);
 
     /**
-     * 查询井盖监测设备列表
+     * 查询井盖监测设备列表(仅查窨井类型设备:排水窨井=9/供水窨井=10),支持分页与报警级别过滤
      * @param listInDTO
      * @return
      */
-    List<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO);
+    Page<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO);
+
+    /**
+     * 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
+     * @param inDTO 筛选条件(区域/设备类型)
+     * @return regionDistribution + typeDistribution + total
+     */
+    Map<String, Object> getManholeDeviceStatistics(ManholeDeviceStatisticsInDTO inDTO);
 
     /**
      * 根据设备类别分组统计设备数量
@@ -79,4 +89,13 @@ public interface EquipmentBaseService extends IService<EquipmentBase> {
      * @return 设备列表
      */
     List<EquipmentBase> findByTopLevelType(String typeName);
-}
+
+    /**
+     * 窨井盖实时监测-设备报警监控列表
+     * 查询所有窨井类型(equipment_type_id IN (9,10))设备的 jg_device_data 最新一条记录
+     * 根据倾斜角度/水浸/水位判定严重告警,低电量/弱信号判定一般告警
+     * @param inDTO 查询条件(keyword 模糊匹配)
+     * @return 设备列表 + 统计信息
+     */
+    AlarmMonitorListOutDTO getAlarmMonitorList(AlarmMonitorListInDTO inDTO);
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/MaintenanceDataStatService.java

@@ -2,10 +2,12 @@ package com.zksy.base.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.base.domain.MaintenanceDataStat;
+import com.zksy.manhole.dto.in.MaintenanceDataStatDashboardInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatQueryInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatTrendInDTO;
 import com.zksy.manhole.dto.out.AbnormalTerminalTrendOutDTO;
 import com.zksy.manhole.dto.out.CaseRateTrendOutDTO;
+import com.zksy.manhole.dto.out.MaintenanceDataStatDashboardOutDTO;
 import com.zksy.manhole.dto.out.MaintenanceDataStatOutDTO;
 
 import java.util.List;
@@ -37,4 +39,11 @@ public interface MaintenanceDataStatService extends IService<MaintenanceDataStat
      * @return
      */
     List<CaseRateTrendOutDTO> caseRateTrend(MaintenanceDataStatTrendInDTO inDTO);
+
+    /**
+     * 运维看板统计数据:一次返回KPI指标、异常终端趋势、处置率/延期率趋势、问题类型分布、区域排行、异常终端列表
+     * @param inDTO
+     * @return
+     */
+    MaintenanceDataStatDashboardOutDTO getDashboardStat(MaintenanceDataStatDashboardInDTO inDTO);
 }

+ 27 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/ManholeLayerService.java

@@ -0,0 +1,27 @@
+package com.zksy.base.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
+import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 态势图层 - 窨井设备服务
+ * 【重要说明】此 Service 专用于态势图层,仅查窨井类型设备(9-排水窨井/10-供水窨井),不包含子设备
+ * 【报警级别】根据 alarm_data 表:alarm_level=1 严重报警,alarm_level>1 一般报警
+ */
+public interface ManholeLayerService {
+
+    /**
+     * 获取态势图层数据(设备列表 + 统计)
+     * @param listInDTO 筛选条件(keyword/currentStatus/alarmStatus/onlineStatus)
+     * @return { devices: 设备列表(含状态、坐标、最新报警),statistics: 状态统计 }
+     */
+    Map<String, Object> getManholeLayerData(ManholeDeviceListInDTO listInDTO);
+
+}

+ 2 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/WorkOrderService.java

@@ -23,9 +23,9 @@ public interface WorkOrderService extends IService<WorkOrder> {
     /**
      * 新增工单
      * @param inDTO
-     * @return
+     * @return 工单ID(新增成功返回 orderId,失败返回 null)
      */
-    boolean addWorkOrder(WorkOrderAddInDTO inDTO);
+    Long addWorkOrder(WorkOrderAddInDTO inDTO);
 
     /**
      * 经办工单分页查询(处理中:待接单、已接单、处理中、待验收)

+ 111 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImpl.java

@@ -48,24 +48,37 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     private EquipmentStatusMapper equipmentStatusMapper;
 
     @Override
-    public IPage<WorkOrder> selectWorkOrderList(Integer orderType, Integer orderLevel, Integer orderStatus, String deviceCode, String keyword, String equipmentType, Integer pageNum, Integer pageSize) {
+    public IPage<WorkOrder> selectWorkOrderList(Integer orderType, Integer orderLevel, String orderStatus, String deviceCode, String keyword, String equipmentType, Integer pageNum, Integer pageSize) {
         Page<WorkOrder> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<WorkOrder> wrapper = new LambdaQueryWrapper<>();
-        // 按设备类型过滤工单:gas-燃气(父类别ID=3),drainage-排水(typeId为drainage的顶级分类)
+        // 按设备类型过滤工单:gas-燃气(父类别ID=3),drainage-排水(typeId为drainage的顶级分类),water-供水;为空查询全部设备工单
         if (StringUtils.isNotEmpty(equipmentType)) {
             if ("gas".equals(equipmentType)) {
                 wrapper.inSql(WorkOrder::getDeviceId, "SELECT eb.equipment_id FROM app_user.equipment_base eb " + "INNER JOIN app_user.equipment_type et ON eb.equipment_type_id = et.id " + "WHERE et.parent_type_id = '3'");
             } else if ("drainage".equals(equipmentType)) {
                 // 排水:typeId为drainage的顶级分类及其全部子类型
                 wrapper.inSql(WorkOrder::getDeviceId, "SELECT eb.equipment_id FROM app_user.equipment_base eb " + "INNER JOIN app_user.equipment_type et ON eb.equipment_type_id = et.id " + "WHERE et.id IN (SELECT id FROM app_user.equipment_type WHERE type_id = 'drainage' AND parent_type_id = '0' " + "UNION SELECT id FROM app_user.equipment_type WHERE parent_type_id = (SELECT id FROM app_user.equipment_type WHERE type_id = 'drainage' AND parent_type_id = '0'))");
+            } else if ("water".equals(equipmentType)) {
+                // 供水:顶级类型名称为“供水”的分类及其全部子类型
+                wrapper.inSql(WorkOrder::getDeviceId, "SELECT eb.equipment_id FROM app_user.equipment_base eb " + "INNER JOIN app_user.equipment_type et ON eb.equipment_type_id = et.id " + "WHERE et.id IN (SELECT id FROM app_user.equipment_type WHERE type_name = '供水' AND parent_type_id = '0' " + "UNION SELECT id FROM app_user.equipment_type WHERE parent_type_id = (SELECT id FROM app_user.equipment_type WHERE type_name = '供水' AND parent_type_id = '0'))");
             }
-        } else {
-            // 默认只查询燃气设备类工单(兼容旧逻辑)
-            wrapper.inSql(WorkOrder::getDeviceId, "SELECT eb.equipment_id FROM app_user.equipment_base eb " + "INNER JOIN app_user.equipment_type et ON eb.equipment_type_id = et.id " + "WHERE et.parent_type_id = '3'");
         }
         if (orderType != null) wrapper.eq(WorkOrder::getOrderType, orderType);
         if (orderLevel != null) wrapper.eq(WorkOrder::getOrderLevel, orderLevel);
-        if (orderStatus != null) wrapper.eq(WorkOrder::getOrderStatus, orderStatus);
+        // orderStatus 支持逗号分隔多值,如 1,2,3
+        if (StringUtils.isNotEmpty(orderStatus)) {
+            if (orderStatus.contains(",")) {
+                List<Integer> statusList = Arrays.stream(orderStatus.split(","))
+                        .filter(StringUtils::isNumeric)
+                        .map(Integer::valueOf)
+                        .collect(Collectors.toList());
+                if (!statusList.isEmpty()) {
+                    wrapper.in(WorkOrder::getOrderStatus, statusList);
+                }
+            } else {
+                wrapper.eq(WorkOrder::getOrderStatus, Integer.valueOf(orderStatus));
+            }
+        }
         if (StringUtils.isNotEmpty(deviceCode)) wrapper.eq(WorkOrder::getDeviceCode, deviceCode);
         if (StringUtils.isNotEmpty(keyword)) {
             wrapper.and(w -> w.like(WorkOrder::getOrderNo, keyword).or().like(WorkOrder::getDeviceCode, keyword).or().like(WorkOrder::getOrderDesc, keyword));
@@ -179,7 +192,7 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean processOrder(Long orderId, String handleResult, String solution, Long operUserId, String operUserName) {
+    public boolean processOrder(Long orderId, String handleResult, String solution, String maintenanceBeforeImg, Long operUserId, String operUserName) {
         WorkOrder order = this.getById(orderId);
         if (order == null) throw new ServiceException("工单不存在");
         if (!Arrays.asList(3, 4).contains(order.getOrderStatus())) throw new ServiceException("当前工单状态不允许处理");
@@ -188,6 +201,9 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         entity.setOrderId(orderId);
         entity.setOrderStatus(4);
         entity.setRepairResult(solution);
+        if (StringUtils.isNotBlank(maintenanceBeforeImg)) {
+            entity.setMaintenanceBeforeImg(maintenanceBeforeImg);
+        }
         this.updateById(entity);
 
         // 工单处理中 → 同步隐患状态为"整改中"
@@ -206,7 +222,7 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean completeOrder(Long orderId, String result, Long operUserId, String operUserName) {
+    public boolean completeOrder(Long orderId, String result, String maintenanceAfterImg, Long operUserId, String operUserName) {
         WorkOrder order = this.getById(orderId);
         if (order == null) throw new ServiceException("工单不存在");
         if (order.getOrderStatus() != 4)
@@ -216,6 +232,9 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         entity.setOrderId(orderId);
         entity.setOrderStatus(5);
         entity.setRepairResult(result);
+        if (StringUtils.isNotBlank(maintenanceAfterImg)) {
+            entity.setMaintenanceAfterImg(maintenanceAfterImg);
+        }
         this.updateById(entity);
 
         WorkOrderLog logEntry = new WorkOrderLog();
@@ -232,6 +251,9 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createWorkOrderFromHazard(String hazardId, String hazardNo, String hazardDesc, String equipmentId, String hazardLevel, String hazardImages, Date planFinishTime) {
+        // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
+        checkDeviceOrderConflict(equipmentId);
+
         // 隐患等级映射到工单优先级:0-重大→紧急(1) 1-较大→一般(2) 2-一般→一般(2) 3-轻微→低(3)
         Integer orderLevel;
         if ("0".equals(hazardLevel)) {
@@ -258,7 +280,10 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         workOrder.setOrderLevel(orderLevel);
         workOrder.setOrderDesc(hazardDesc);
         workOrder.setOrderStatus(1); // 待派单
-        workOrder.setWorkImage(hazardImages);
+        // 隐患图片作为维修前图片(如有)
+        if (StringUtils.isNotEmpty(hazardImages)) {
+            workOrder.setMaintenanceBeforeImg(hazardImages);
+        }
         workOrder.setPlanFinishTime(planFinishTime);
         workOrder.setCreateTime(new Date());
         this.save(workOrder);
@@ -275,6 +300,59 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         return workOrder.getOrderId();
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createWorkOrder(WorkOrder workOrder) {
+        // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
+        checkDeviceOrderConflict(workOrder.getDeviceId());
+
+        if (workOrder.getOrderType() == null) {
+            workOrder.setOrderType(1);
+        }
+        if (workOrder.getOrderLevel() == null) {
+            workOrder.setOrderLevel(2);
+        }
+        workOrder.setOrderNo(generateOrderNo());
+        // 设备ID存在但未填设备编码时自动补全
+        if (StringUtils.isNotEmpty(workOrder.getDeviceId()) && StringUtils.isEmpty(workOrder.getDeviceCode())) {
+            EquipmentBase equipment = equipmentBaseMapper.selectById(workOrder.getDeviceId());
+            if (equipment != null) {
+                workOrder.setDeviceCode(equipment.getEquipmentCode());
+            }
+        }
+        workOrder.setOrderStatus(1); // 待派单
+        workOrder.setCreateTime(new Date());
+        this.save(workOrder);
+
+        // 写入操作日志
+        WorkOrderLog logEntry = new WorkOrderLog();
+        logEntry.setOrderId(workOrder.getOrderId());
+        logEntry.setOperType(1);
+        logEntry.setOperDesc("手动创建工单");
+        logEntry.setOperTime(new Date());
+        workOrderLogMapper.insert(logEntry);
+
+        log.info("手动创建工单成功,工单ID={},类型={}", workOrder.getOrderId(), workOrder.getOrderType());
+        return workOrder.getOrderId();
+    }
+
+    /**
+     * 校验设备是否存在进行中工单(状态 1-5/8/9),存在则禁止再次创建工单
+     * 业务规则:同一设备同一时间只能有一个进行中的工单,结案(6)或驳回(7)后方可再次创建
+     */
+    private void checkDeviceOrderConflict(String deviceId) {
+        if (StringUtils.isEmpty(deviceId)) {
+            return;
+        }
+        Long count = this.lambdaQuery()
+                .eq(WorkOrder::getDeviceId, deviceId)
+                .notIn(WorkOrder::getOrderStatus, 6, 7)
+                .count();
+        if (count > 0) {
+            throw new ServiceException("该设备已有处理中的工单,需结案或驳回后才能创建新工单");
+        }
+    }
+
     /**
      * 生成工单编号:WO-yyyyMMddHHmmssSSS
      */
@@ -305,6 +383,30 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         return true;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean rejectOrder(Long orderId, String reason, Long operUserId, String operUserName) {
+        WorkOrder order = this.getById(orderId);
+        if (order == null) throw new ServiceException("工单不存在");
+        // 仅待派单/待接单可驳回,已流转的工单请使用取消
+        if (!Arrays.asList(1, 2).contains(order.getOrderStatus())) throw new ServiceException("当前工单状态不允许驳回");
+
+        WorkOrder entity = new WorkOrder();
+        entity.setOrderId(orderId);
+        entity.setOrderStatus(7);
+        this.updateById(entity);
+
+        WorkOrderLog logEntry = new WorkOrderLog();
+        logEntry.setOrderId(orderId);
+        logEntry.setOperType(7);
+        logEntry.setOperDesc("驳回工单,原因:" + (reason != null ? reason : ""));
+        logEntry.setOperUserId(operUserId);
+        logEntry.setOperUserName(operUserName);
+        logEntry.setOperTime(new Date());
+        workOrderLogMapper.insert(logEntry);
+        return true;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean closeOrder(Long orderId, String result, Long operUserId, String operUserName) {

+ 405 - 52
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/EquipmentBaseServiceImpl.java

@@ -16,10 +16,12 @@ import com.zksy.base.manhole.mapper.ManholeDataMapper;
 import com.zksy.base.mapper.*;
 import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.manhole.dto.in.AlarmMonitorListInDTO;
 import com.zksy.manhole.dto.in.EquipmentCountByTypeInDTO;
 import com.zksy.manhole.dto.in.EquipmentExportInDTO;
 import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
 import com.zksy.manhole.dto.in.ManholeDevicePageInDTO;
+import com.zksy.manhole.dto.in.ManholeDeviceStatisticsInDTO;
 import com.zksy.manhole.dto.out.*;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -27,6 +29,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.function.Function;
@@ -67,6 +71,9 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
     @Autowired
     private PipeNetworkBaseMapper pipeNetworkBaseMapper;
 
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
     @Override
     public Page<EquipmentBase> findByPage(long pageNum, long pageSize,
                                           String equipmentCode, String equipmentName,
@@ -256,7 +263,7 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
      */
     @Override
     public Page<EquipmentBaseOutDTO> findManholeDeviceByPage(ManholeDevicePageInDTO pageInDTO) {
-        log.info("分页查询监测设备台账列表-入参:{}", pageInDTO);
+        //log.info("分页查询监测设备台账列表-入参:{}", pageInDTO);
         List<String> equipmentIds = null;
         if(pageInDTO.getCurrentStatus() != null){
             List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(new LambdaQueryWrapper<EquipmentStatus>()
@@ -268,8 +275,11 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
         }
         Page<EquipmentBase> page = new Page<>(pageInDTO.getPageNum(), pageInDTO.getPageSize());
         LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(StringUtils.isNotEmpty(pageInDTO.getDeviceCode()), EquipmentBase::getEquipmentCode, pageInDTO.getDeviceCode())
-                .eq(StringUtils.isNotEmpty(pageInDTO.getEquipmentTypeId()), EquipmentBase::getEquipmentTypeId, pageInDTO.getEquipmentTypeId())
+        // 窨井盖设备类型过滤:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10),筛选时仅查所选类别
+        List<String> manholeTypeIds = StringUtils.isNotEmpty(pageInDTO.getEquipmentTypeId())
+                ? Collections.singletonList(pageInDTO.getEquipmentTypeId()) : Arrays.asList("5", "9", "10");
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .eq(StringUtils.isNotEmpty(pageInDTO.getDeviceCode()), EquipmentBase::getEquipmentCode, pageInDTO.getDeviceCode())
                 .in(CollUtil.isNotEmpty(equipmentIds), EquipmentBase::getEquipmentId, equipmentIds)
                 .like(StringUtils.isNotEmpty(pageInDTO.getDeviceName()), EquipmentBase::getEquipmentName, pageInDTO.getDeviceName())
                 .like(StringUtils.isNotEmpty(pageInDTO.getEquipmentLocation()), EquipmentBase::getEquipmentLocation, pageInDTO.getEquipmentLocation())
@@ -330,13 +340,78 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
         return outPage;
     }
 
+    /**
+     * 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
+     * @param inDTO 筛选条件(区域/设备类型)
+     * @return regionDistribution + typeDistribution + total
+     */
+    @Override
+    public Map<String, Object> getManholeDeviceStatistics(ManholeDeviceStatisticsInDTO inDTO) {
+        // 窨井设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10),筛选时仅统计所选类别
+        List<String> manholeTypeIds = StringUtils.isNotEmpty(inDTO.getEquipmentTypeId())
+                ? Collections.singletonList(inDTO.getEquipmentTypeId()) : Arrays.asList("5", "9", "10");
+        List<EquipmentBase> devices = this.list(new LambdaQueryWrapper<EquipmentBase>()
+                .in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .eq(StringUtils.isNotEmpty(inDTO.getDistrict()), EquipmentBase::getDistrict, inDTO.getDistrict()));
+        int total = devices.size();
+
+        // 区域分布:按所属片区(district)分组统计
+        Map<String, Long> regionCountMap = devices.stream()
+                .filter(d -> StringUtils.isNotBlank(d.getDistrict()))
+                .collect(Collectors.groupingBy(EquipmentBase::getDistrict, Collectors.counting()));
+        List<Map<String, Object>> regionDistribution = regionCountMap.entrySet().stream()
+                .sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
+                .map(e -> {
+                    Map<String, Object> item = new HashMap<>();
+                    item.put("name", e.getKey());
+                    item.put("count", e.getValue());
+                    item.put("percent", calcPercent(e.getValue(), total));
+                    return item;
+                })
+                .collect(Collectors.toList());
+
+        // 类型分布:按设备类型分组统计(9=排水窨井设备,10=供水窨井设备)
+        Map<String, String> typeNameMap = new HashMap<>();
+        typeNameMap.put("9", "排水窨井设备");
+        typeNameMap.put("10", "供水窨井设备");
+        Map<String, Long> typeCountMap = devices.stream()
+                .filter(d -> StringUtils.isNotBlank(d.getEquipmentTypeId()))
+                .collect(Collectors.groupingBy(EquipmentBase::getEquipmentTypeId, Collectors.counting()));
+        List<Map<String, Object>> typeDistribution = typeCountMap.entrySet().stream()
+                .sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
+                .map(e -> {
+                    Map<String, Object> item = new HashMap<>();
+                    item.put("name", typeNameMap.getOrDefault(e.getKey(), e.getKey()));
+                    item.put("count", e.getValue());
+                    item.put("percent", calcPercent(e.getValue(), total));
+                    return item;
+                })
+                .collect(Collectors.toList());
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("total", total);
+        result.put("regionDistribution", regionDistribution);
+        result.put("typeDistribution", typeDistribution);
+        return result;
+    }
+
+    /**
+     * 计算占比(保留1位小数)
+     */
+    private BigDecimal calcPercent(Long count, int total) {
+        if (total <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return new BigDecimal(count * 100.0 / total).setScale(1, RoundingMode.HALF_UP);
+    }
+
     /**
      * 查询井盖监测设备列表
      * @param listInDTO
      * @return
      */
     @Override
-    public List<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO) {
+    public Page<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO) {
         log.info("查询井盖监测设备列表-入参:{}", listInDTO);
         List<String> equipmentIds = null;
         if(listInDTO.getCurrentStatus() != null || listInDTO.getAlarmStatus() != null || listInDTO.getOnlineStatus() != null){
@@ -345,12 +420,18 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
                     .eq(listInDTO.getAlarmStatus() != null, EquipmentStatus::getAlarmStatus, listInDTO.getAlarmStatus())
                     .eq(listInDTO.getOnlineStatus() != null, EquipmentStatus::getOnlineStatus, listInDTO.getOnlineStatus()));
             if(CollUtil.isEmpty(equipmentStatusList)){
-                return new ArrayList<>();
+                return new Page<>(1, 10, 0);
             }
             equipmentIds = equipmentStatusList.stream().map(EquipmentStatus::getEquipmentId).distinct().collect(Collectors.toList());
         }
+        // 仅查询窨井类型设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10)
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+        // 按报警级别过滤时需要 manholeData 参与判定,须全量查出后内存过滤+内存分页
+        boolean needLevelFilter = listInDTO.getAlarmLevel() != null;
+        boolean needManholeData = Boolean.TRUE.equals(listInDTO.getIsQueryManholeData()) || needLevelFilter;
         LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.in(CollUtil.isNotEmpty(equipmentIds), EquipmentBase::getEquipmentId, equipmentIds)
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .in(CollUtil.isNotEmpty(equipmentIds), EquipmentBase::getEquipmentId, equipmentIds)
                 .and(StringUtils.isNotBlank(listInDTO.getKeyword()), wrapper -> wrapper
                         .like(EquipmentBase::getEquipmentCode, listInDTO.getKeyword())
                         .or()
@@ -358,59 +439,321 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
                         .or()
                         .like(EquipmentBase::getEquipmentLocation, listInDTO.getKeyword()))
                 .orderByDesc(EquipmentBase::getUpdateTime);
-        List<EquipmentBase> equipmentBaseList = this.list(queryWrapper);
-        if (CollUtil.isEmpty(equipmentBaseList)) {
+
+        Page<EquipmentBase> rs;
+        if (needLevelFilter) {
+            // 报警级别需基于最新监测数据判定,先全量查询再内存过滤/分页
+            List<EquipmentBase> allList = this.list(queryWrapper);
+            rs = new Page<>(1, Math.max(allList.size(), 1), allList.size());
+            rs.setRecords(allList);
+        } else if (listInDTO.getPageNum() != null && listInDTO.getPageSize() != null) {
+            Page<EquipmentBase> page = new Page<>(listInDTO.getPageNum(), listInDTO.getPageSize());
+            rs = this.page(page, queryWrapper);
+        } else {
+            List<EquipmentBase> allList = this.list(queryWrapper);
+            rs = new Page<>(1, Math.max(allList.size(), 1), allList.size());
+            rs.setRecords(allList);
+        }
+        List<EquipmentBaseOutDTO> outDTOList = BeanUtil.copyToList(rs.getRecords(), EquipmentBaseOutDTO.class);
+        if (CollUtil.isNotEmpty(outDTOList)) {
+            //查询设备类型
+            List<String> equipmentTypeIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId).distinct().collect(Collectors.toList());
+            LambdaQueryWrapper<EquipmentType> batchQw = new LambdaQueryWrapper<>();
+            batchQw.in(EquipmentType::getId, equipmentTypeIdList);
+            List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(batchQw);
+            Map<String, EquipmentType> equipmentTypeMap = new HashMap<>();
+            if(CollUtil.isNotEmpty(equipmentTypeList)){
+                equipmentTypeMap.putAll(equipmentTypeList.stream().collect(
+                    Collectors.toMap(t -> String.valueOf(t.getId()), Function.identity(), (key1, key2) -> key2)));
+            }
+            //查询设备状态
+            List<String> equipmentIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentId).distinct().collect(Collectors.toList());
+            List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(new LambdaQueryWrapper<EquipmentStatus>()
+                    .in(EquipmentStatus::getEquipmentId, equipmentIdList));
+            Map<String, EquipmentStatus> equipmentStatusMap = new HashMap<>();
+            if(CollUtil.isNotEmpty(equipmentStatusList)){
+                equipmentStatusMap.putAll(equipmentStatusList.stream().collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (key1, key2) -> key2)));
+            }
+            //查询设备监测状态
+            Map<String, List<ManholeData>> imeiCardNumberMapList = new HashMap<>();
+            if(needManholeData){
+                List<String> equipmentCodeList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
+                List<ManholeData> manholeDataList = manholeDataMapper.selectList(new LambdaQueryWrapper<ManholeData>()
+                        .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                        .orderByDesc(ManholeData::getCreateTime));
+                imeiCardNumberMapList.putAll(manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber)));
+            }
+            List<ManholeData> manholeDataList = null;
+            for(EquipmentBaseOutDTO outDTO : outDTOList) {
+                EquipmentType equipmentType = equipmentTypeMap.get(outDTO.getEquipmentTypeId());
+                if(equipmentType != null){
+                    outDTO.setEquipmentTypeName(equipmentType.getTypeName());
+                }
+                EquipmentStatus equipmentStatus = equipmentStatusMap.get(outDTO.getEquipmentId());
+                if(equipmentStatus != null){
+                    EquipmentStatusOutDTO equipmentStatusOutDTO = BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class);
+                    outDTO.setEquipmentStatus(equipmentStatusOutDTO);
+                }
+                if(needManholeData){
+                    manholeDataList = imeiCardNumberMapList.get(outDTO.getEquipmentCode());
+                    if(CollUtil.isNotEmpty(manholeDataList)){
+                        ManholeData manholeData = manholeDataList.get(0);
+                        ManholeDataOutDTO manholeDataOutDTO = BeanUtil.copyProperties(manholeData, ManholeDataOutDTO.class);
+                        outDTO.setManholeData(manholeDataOutDTO);
+                    }
+                }
+            }
+        }
+
+        long total;
+        List<EquipmentBaseOutDTO> pageRecords;
+        if (needLevelFilter) {
+            // 内存过滤报警级别
+            List<EquipmentBaseOutDTO> filteredList = outDTOList.stream()
+                    .filter(dto -> matchAlarmLevel(dto, listInDTO.getAlarmLevel()))
+                    .collect(Collectors.toList());
+            total = filteredList.size();
+            pageRecords = applyPage(filteredList, listInDTO.getPageNum(), listInDTO.getPageSize());
+        } else {
+            total = rs.getTotal();
+            pageRecords = outDTOList;
+        }
+        long pageSize = listInDTO.getPageSize() != null ? listInDTO.getPageSize()
+                : (total > 0 ? total : 1);
+        Page<EquipmentBaseOutDTO> outPage = new Page<>(listInDTO.getPageNum() != null ? listInDTO.getPageNum() : 1, pageSize, total);
+        outPage.setRecords(pageRecords);
+        return outPage;
+    }
+
+    /**
+     * 报警级别判定(与前端 buildAlertInfo 保持一致):
+     * 严重报警(1):设备报警状态=1 或 水位/水浸报警;
+     * 一般报警(2):非严重报警且 低电量(<=20%)或 弱信号(<=20%);
+     * 有报警(0):严重报警或一般报警的并集;不传则不过滤。
+     */
+    private boolean matchAlarmLevel(EquipmentBaseOutDTO dto, Integer alarmLevel) {
+        if (alarmLevel == null) {
+            return true;
+        }
+        boolean isCritical = (dto.getEquipmentStatus() != null
+                && Integer.valueOf(1).equals(dto.getEquipmentStatus().getAlarmStatus()))
+                || (dto.getManholeData() != null
+                && ("1".equals(dto.getManholeData().getWaterLevelAlarmStatus())
+                || "1".equals(dto.getManholeData().getWaterInfiltrationAlarmStatus())));
+        if (Integer.valueOf(1).equals(alarmLevel)) {
+            return isCritical;
+        }
+        if (dto.getManholeData() == null) {
+            return Integer.valueOf(0).equals(alarmLevel) && isCritical;
+        }
+        boolean lowBattery = dto.getManholeData().getBatteryLevel() != null
+                && toInt(dto.getManholeData().getBatteryLevel()) <= 20;
+        boolean weakSignal = dto.getManholeData().getSignalStrength() != null
+                && toInt(dto.getManholeData().getSignalStrength()) <= 20;
+        boolean hasAlarm = isCritical || lowBattery || weakSignal;
+        if (Integer.valueOf(0).equals(alarmLevel)) {
+            return hasAlarm;
+        }
+        // alarmLevel = 2:一般报警
+        return hasAlarm && !isCritical;
+    }
+
+    private int toInt(String value) {
+        try {
+            return (int) Double.parseDouble(value.trim());
+        } catch (Exception e) {
+            return Integer.MAX_VALUE;
+        }
+    }
+
+    /**
+     * 内存分页(报警级别过滤后使用)
+     */
+    private List<EquipmentBaseOutDTO> applyPage(List<EquipmentBaseOutDTO> list, Long pageNum, Long pageSize) {
+        if (pageNum == null || pageSize == null) {
+            return list;
+        }
+        int start = (int) ((pageNum - 1) * pageSize);
+        if (start >= list.size()) {
             return new ArrayList<>();
         }
-        List<EquipmentBaseOutDTO> outDTOList = BeanUtil.copyToList(equipmentBaseList, EquipmentBaseOutDTO.class);
-        //查询设备类型
-        List<String> equipmentTypeIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId).distinct().collect(Collectors.toList());
-        LambdaQueryWrapper<EquipmentType> batchQw = new LambdaQueryWrapper<>();
-        batchQw.in(EquipmentType::getId, equipmentTypeIdList);
-        List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(batchQw);
+        int end = (int) Math.min(start + pageSize, list.size());
+        return new ArrayList<>(list.subList(start, end));
+    }
+
+    /**
+     * 窨井盖实时监测-设备报警监控列表
+     * 查询所有窨井类型设备(equipment_type_id IN (9,10))的 jg_device_data 最新一条记录
+     * 告警等级判定:
+     *   严重告警(critical) — 倾斜角度超阈值 / 水浸报警 / 水位报警
+     *   一般告警(warning)  — 低电量(battery<=20) / 弱信号(signal<=20)
+     */
+    @Override
+    public AlarmMonitorListOutDTO getAlarmMonitorList(AlarmMonitorListInDTO inDTO) {
+        log.info("窨井盖实时监测-设备报警监控列表入参:{}", inDTO);
+        // 仅查询窨井类型设备:排水窨井(9)/供水窨井(10)
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+        LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .and(StringUtils.isNotBlank(inDTO.getKeyword()), w -> w
+                        .like(EquipmentBase::getEquipmentCode, inDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentName, inDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentLocation, inDTO.getKeyword()))
+                .orderByDesc(EquipmentBase::getUpdateTime);
+        List<EquipmentBase> equipmentList = this.list(queryWrapper);
+        if (CollUtil.isEmpty(equipmentList)) {
+            AlarmMonitorListOutDTO emptyResult = new AlarmMonitorListOutDTO();
+            emptyResult.setDevices(new ArrayList<>());
+            Map<String, Object> emptyStats = new HashMap<>();
+            emptyStats.put("total", 0);
+            emptyStats.put("critical", 0);
+            emptyStats.put("warning", 0);
+            emptyStats.put("normal", 0);
+            emptyStats.put("onlineCount", 0);
+            emptyStats.put("avgBattery", 0);
+            emptyResult.setStatistics(emptyStats);
+            return emptyResult;
+        }
+        List<EquipmentBaseOutDTO> outDTOList = BeanUtil.copyToList(equipmentList, EquipmentBaseOutDTO.class);
+
+        // 批量查询设备类型名称
+        List<String> typeIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId).distinct().collect(Collectors.toList());
+        List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(new LambdaQueryWrapper<EquipmentType>()
+                .in(EquipmentType::getId, typeIdList));
         Map<String, EquipmentType> equipmentTypeMap = new HashMap<>();
-        if(CollUtil.isNotEmpty(equipmentTypeList)){
-            equipmentTypeMap.putAll(equipmentTypeList.stream().collect(
-                Collectors.toMap(t -> String.valueOf(t.getId()), Function.identity(), (key1, key2) -> key2)));
+        if (CollUtil.isNotEmpty(equipmentTypeList)) {
+            equipmentTypeMap.putAll(equipmentTypeList.stream().collect(Collectors.toMap(t -> String.valueOf(t.getId()), Function.identity(), (k1, k2) -> k2)));
         }
-        //查询设备状态
+
+        // 批量查询设备运维状态
         List<String> equipmentIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentId).distinct().collect(Collectors.toList());
         List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(new LambdaQueryWrapper<EquipmentStatus>()
                 .in(EquipmentStatus::getEquipmentId, equipmentIdList));
-        Map<String, EquipmentStatus> EquipmentStatusMap = new HashMap<>();
-        if(CollUtil.isNotEmpty(equipmentStatusList)){
-            EquipmentStatusMap.putAll(equipmentStatusList.stream().collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (key1, key2) -> key2)));
-        }
-        //查询设备监测状态
-        Map<String, List<ManholeData>> imeiCardNumberMapList = new HashMap<>();
-        if(listInDTO.getIsQueryManholeData()){
-            List<String> equipmentCodeList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
-            List<ManholeData> manholeDataList = manholeDataMapper.selectList(new LambdaQueryWrapper<ManholeData>()
-                    .in(ManholeData::getImeiCardNumber, equipmentCodeList)
-                    .orderByDesc(ManholeData::getCreateTime));
-            imeiCardNumberMapList.putAll(manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber)));
-        }
-        List<ManholeData> manholeDataList = null;
-        for(EquipmentBaseOutDTO outDTO : outDTOList) {
+        Map<String, EquipmentStatus> equipmentStatusMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentStatusList)) {
+            equipmentStatusMap.putAll(equipmentStatusList.stream().collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (k1, k2) -> k2)));
+        }
+
+        // 批量查询最新 jg_device_data 记录(按 imei_card_number = equipment_code 关联)
+        List<String> equipmentCodeList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
+        List<ManholeData> manholeDataList = manholeDataMapper.selectList(new LambdaQueryWrapper<ManholeData>()
+                .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                .orderByDesc(ManholeData::getCreateTime));
+        Map<String, List<ManholeData>> imeiMapList = new HashMap<>();
+        if (CollUtil.isNotEmpty(manholeDataList)) {
+            imeiMapList.putAll(manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber)));
+        }
+
+        // 批量查询未办结工单(待派单/待接单/已接单/处理中/待验收/延期审核)
+        List<Integer> activeStatusList = Arrays.asList(1, 2, 3, 4, 5, 8);
+        List<WorkOrder> activeWorkOrders = workOrderMapper.selectList(new LambdaQueryWrapper<WorkOrder>()
+                .in(WorkOrder::getDeviceId, equipmentIdList)
+                .in(WorkOrder::getOrderStatus, activeStatusList));
+        Set<String> deviceWithActiveOrder = activeWorkOrders.stream()
+                .map(WorkOrder::getDeviceId)
+                .collect(Collectors.toSet());
+
+        // 统计变量
+        int critical = 0, warning = 0, normal = 0, onlineCount = 0;
+        double batterySum = 0;
+        int batteryCount = 0;
+
+        for (EquipmentBaseOutDTO outDTO : outDTOList) {
+            // 设备类型名称
             EquipmentType equipmentType = equipmentTypeMap.get(outDTO.getEquipmentTypeId());
-            if(equipmentType != null){
+            if (equipmentType != null) {
                 outDTO.setEquipmentTypeName(equipmentType.getTypeName());
             }
-            EquipmentStatus equipmentStatus = EquipmentStatusMap.get(outDTO.getEquipmentId());
-            if(equipmentStatus != null){
-                EquipmentStatusOutDTO equipmentStatusOutDTO = BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class);
-                outDTO.setEquipmentStatus(equipmentStatusOutDTO);
+            // 设备运维状态
+            EquipmentStatus equipmentStatus = equipmentStatusMap.get(outDTO.getEquipmentId());
+            if (equipmentStatus != null) {
+                EquipmentStatusOutDTO statusOutDTO = BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class);
+                outDTO.setEquipmentStatus(statusOutDTO);
+                if (Integer.valueOf(1).equals(equipmentStatus.getOnlineStatus())) {
+                    onlineCount++;
+                }
             }
-            if(listInDTO.getIsQueryManholeData()){
-                manholeDataList = imeiCardNumberMapList.get(outDTO.getEquipmentCode());
-                if(CollUtil.isNotEmpty(manholeDataList)){
-                    ManholeData manholeData = manholeDataList.get(0);
-                    ManholeDataOutDTO manholeDataOutDTO = BeanUtil.copyProperties(manholeData, ManholeDataOutDTO.class);
-                    outDTO.setManholeData(manholeDataOutDTO);
+            // 是否有未办结工单
+            outDTO.setWorkOrderSubmitted(deviceWithActiveOrder.contains(outDTO.getEquipmentId()));
+            // 最新 jg_device_data 记录
+            List<ManholeData> dataList = imeiMapList.get(outDTO.getEquipmentCode());
+            if (CollUtil.isNotEmpty(dataList)) {
+                ManholeData latest = dataList.get(0);
+                ManholeDataOutDTO manholeDataOutDTO = BeanUtil.copyProperties(latest, ManholeDataOutDTO.class);
+                outDTO.setManholeData(manholeDataOutDTO);
+
+                // 统计电量
+                if (StringUtils.isNotBlank(latest.getBatteryLevel())) {
+                    try {
+                        batterySum += Double.parseDouble(latest.getBatteryLevel().trim());
+                        batteryCount++;
+                    } catch (NumberFormatException ignored) {}
                 }
+
+                // 告警等级判定
+                boolean isCritical = isCriticalAlarm(latest);
+                boolean isWarning = !isCritical && isGeneralAlarm(latest);
+                if (isCritical) critical++;
+                else if (isWarning) warning++;
+                else normal++;
+            } else {
+                normal++;
             }
         }
-        return outDTOList;
+
+        // 构建统计信息
+        Map<String, Object> statistics = new HashMap<>();
+        statistics.put("total", outDTOList.size());
+        statistics.put("critical", critical);
+        statistics.put("warning", warning);
+        statistics.put("normal", normal);
+        statistics.put("onlineCount", onlineCount);
+        statistics.put("avgBattery", batteryCount > 0 ? (int) (batterySum / batteryCount) : 0);
+
+        AlarmMonitorListOutDTO result = new AlarmMonitorListOutDTO();
+        result.setDevices(outDTOList);
+        result.setStatistics(statistics);
+        return result;
+    }
+
+    /**
+     * 严重告警判定:倾斜角度超阈值 / 水浸报警 / 水位报警
+     */
+    private boolean isCriticalAlarm(ManholeData data) {
+        if (data == null) return false;
+        // 水浸报警
+        if ("1".equals(data.getWaterInfiltrationAlarmStatus())) return true;
+        // 水位报警
+        if ("1".equals(data.getWaterLevelAlarmStatus())) return true;
+        // 倾斜角度超阈值
+        if (StringUtils.isNotBlank(data.getTiltAngle()) && StringUtils.isNotBlank(data.getAngleAlarmThreshold())) {
+            try {
+                double angle = Double.parseDouble(data.getTiltAngle().trim());
+                double threshold = Double.parseDouble(data.getAngleAlarmThreshold().trim());
+                if (angle > threshold) return true;
+            } catch (NumberFormatException ignored) {}
+        }
+        return false;
+    }
+
+    /**
+     * 一般告警判定:低电量(battery<=20) / 弱信号(signal<=20)
+     */
+    private boolean isGeneralAlarm(ManholeData data) {
+        if (data == null) return false;
+        if (StringUtils.isNotBlank(data.getBatteryLevel())) {
+            try {
+                if (Double.parseDouble(data.getBatteryLevel().trim()) <= 20) return true;
+            } catch (NumberFormatException ignored) {}
+        }
+        if (StringUtils.isNotBlank(data.getSignalStrength())) {
+            try {
+                if (Double.parseDouble(data.getSignalStrength().trim()) <= 20) return true;
+            } catch (NumberFormatException ignored) {}
+        }
+        return false;
     }
 
     @Override
@@ -828,15 +1171,25 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
             return new ArrayList<>();
         }
 
-        List<String> typeIds = new ArrayList<>();
-        List<EquipmentType> children = equipmentTypeMapper.selectList(
-                new LambdaQueryWrapper<EquipmentType>()
-                        .eq(EquipmentType::getParentTypeId, topType.getId()));
-        for (EquipmentType child : children) {
-            typeIds.add(child.getId());
+        // 设备类型可能存在多级子分类,查询整个模块的类型树,避免漏掉深层燃气设备。
+        Set<String> typeIds = new LinkedHashSet<>();
+        Set<String> parentIds = new LinkedHashSet<>();
+        typeIds.add(topType.getId());
+        parentIds.add(topType.getId());
+        while (!parentIds.isEmpty()) {
+            List<EquipmentType> children = equipmentTypeMapper.selectList(
+                    new LambdaQueryWrapper<EquipmentType>()
+                            .in(EquipmentType::getParentTypeId, parentIds));
+            Set<String> nextParentIds = new LinkedHashSet<>();
+            for (EquipmentType child : children) {
+                if (typeIds.add(child.getId())) {
+                    nextParentIds.add(child.getId());
+                }
+            }
+            parentIds = nextParentIds;
         }
         LambdaQueryWrapper<EquipmentBase> wrapper = new LambdaQueryWrapper<>();
-        wrapper.in(EquipmentBase::getEquipmentTypeId, typeIds);
+        wrapper.in(EquipmentBase::getEquipmentTypeId, new ArrayList<>(typeIds));
         wrapper.orderByDesc(EquipmentBase::getCreateTime);
         return this.list(wrapper);
     }

+ 429 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/MaintenanceDataStatServiceImpl.java

@@ -4,16 +4,33 @@ import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentStatus;
+import com.zksy.base.domain.EquipmentType;
 import com.zksy.base.domain.MaintenanceDataStat;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.manhole.domain.ManholeData;
+import com.zksy.base.manhole.mapper.ManholeDataMapper;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.EquipmentStatusMapper;
+import com.zksy.base.mapper.EquipmentTypeMapper;
 import com.zksy.base.mapper.MaintenanceDataStatMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
 import com.zksy.base.service.MaintenanceDataStatService;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.manhole.dto.in.MaintenanceDataStatDashboardInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatQueryInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatTrendInDTO;
 import com.zksy.manhole.dto.out.AbnormalTerminalTrendOutDTO;
 import com.zksy.manhole.dto.out.CaseRateTrendOutDTO;
+import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
+import com.zksy.manhole.dto.out.EquipmentStatusOutDTO;
+import com.zksy.manhole.dto.out.MaintenanceDataStatDashboardOutDTO;
 import com.zksy.manhole.dto.out.MaintenanceDataStatOutDTO;
+import com.zksy.manhole.dto.out.ManholeDataOutDTO;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
@@ -23,6 +40,8 @@ import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.WeekFields;
 import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
 * @author Administrator
@@ -34,6 +53,21 @@ import java.util.*;
 public class MaintenanceDataStatServiceImpl extends ServiceImpl<MaintenanceDataStatMapper, MaintenanceDataStat>
     implements MaintenanceDataStatService{
 
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private EquipmentTypeMapper equipmentTypeMapper;
+
+    @Autowired
+    private EquipmentStatusMapper equipmentStatusMapper;
+
+    @Autowired
+    private ManholeDataMapper manholeDataMapper;
+
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
     /**
      * 根据日期查询运维数据统计
      * @param queryInDTO
@@ -128,6 +162,401 @@ public class MaintenanceDataStatServiceImpl extends ServiceImpl<MaintenanceDataS
         return result;
     }
 
+    /**
+     * 运维看板统计数据:一次返回KPI指标、异常终端趋势、处置率/延期率趋势、问题类型分布、区域排行、异常终端列表
+     * 数据来源:
+     *   - KPI/问题类型分布/区域排行/异常终端列表:equipment_base + equipment_status + jg_device_data + work_order 实时计算
+     *   - 趋势:日期范围内 jg_device_data 历史记录与 work_order 按周度/月度分组统计
+     * @param inDTO
+     * @return
+     */
+    @Override
+    public MaintenanceDataStatDashboardOutDTO getDashboardStat(MaintenanceDataStatDashboardInDTO inDTO) {
+        log.info("运维看板统计数据-入参:{}", inDTO);
+        MaintenanceDataStatDashboardOutDTO outDTO = new MaintenanceDataStatDashboardOutDTO();
+
+        // ============ 日期范围:默认近6个月 ============
+        Date endDate = inDTO.getEndDate() != null ? inDTO.getEndDate() : new Date();
+        Date startDate = inDTO.getStartDate();
+        if (startDate == null) {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(endDate);
+            cal.add(Calendar.MONTH, -5);
+            cal.set(Calendar.DAY_OF_MONTH, 1);
+            startDate = cal.getTime();
+        }
+        int trendType = inDTO.getTrendType() != null ? inDTO.getTrendType() : 2;
+
+        // ============ 1. 查询窨井设备(排水9/供水10/燃气5) ============
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectList(
+                new LambdaQueryWrapper<EquipmentBase>()
+                        .in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                        .orderByDesc(EquipmentBase::getUpdateTime));
+        if (CollUtil.isEmpty(equipmentList)) {
+            outDTO.setKpi(buildEmptyKpi());
+            return outDTO;
+        }
+        List<EquipmentBaseOutDTO> deviceList = BeanUtil.copyToList(equipmentList, EquipmentBaseOutDTO.class);
+        List<String> equipmentIdList = deviceList.stream().map(EquipmentBaseOutDTO::getEquipmentId).distinct().collect(Collectors.toList());
+        List<String> equipmentCodeList = deviceList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
+
+        // ============ 2. 批量查询设备类型名称 ============
+        Map<String, String> typeNameMap = new HashMap<>();
+        List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(
+                new LambdaQueryWrapper<EquipmentType>().in(EquipmentType::getId, equipmentTypeIdsOf(deviceList)));
+        if (CollUtil.isNotEmpty(equipmentTypeList)) {
+            typeNameMap = equipmentTypeList.stream()
+                    .collect(Collectors.toMap(t -> String.valueOf(t.getId()), EquipmentType::getTypeName, (a, b) -> b));
+        }
+
+        // ============ 3. 批量查询设备运维状态 ============
+        Map<String, EquipmentStatus> equipmentStatusMap = new HashMap<>();
+        List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(
+                new LambdaQueryWrapper<EquipmentStatus>().in(EquipmentStatus::getEquipmentId, equipmentIdList));
+        if (CollUtil.isNotEmpty(equipmentStatusList)) {
+            equipmentStatusMap = equipmentStatusList.stream()
+                    .collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (a, b) -> a));
+        }
+
+        // ============ 4. 批量查询最新 jg_device_data 记录 ============
+        Map<String, List<ManholeData>> imeiDataMap = new HashMap<>();
+        List<ManholeData> manholeDataList = manholeDataMapper.selectList(
+                new LambdaQueryWrapper<ManholeData>()
+                        .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                        .orderByDesc(ManholeData::getCreateTime));
+        if (CollUtil.isNotEmpty(manholeDataList)) {
+            imeiDataMap = manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber));
+        }
+
+        // ============ 5. 批量查询未办结工单(待派单/待接单/已接单/处理中/待验收/延期审核) ============
+        List<Integer> activeStatusList = Arrays.asList(1, 2, 3, 4, 5, 8);
+        List<WorkOrder> activeWorkOrders = workOrderMapper.selectList(
+                new LambdaQueryWrapper<WorkOrder>()
+                        .in(WorkOrder::getDeviceId, equipmentIdList)
+                        .in(WorkOrder::getOrderStatus, activeStatusList));
+        Set<String> deviceWithActiveOrder = CollUtil.isNotEmpty(activeWorkOrders)
+                ? activeWorkOrders.stream().map(WorkOrder::getDeviceId).collect(Collectors.toSet())
+                : new HashSet<>();
+
+        // ============ 6. 逐设备判定异常并组装 ============
+        List<EquipmentBaseOutDTO> abnormalDeviceList = new ArrayList<>();
+        int abnormalCount = 0, submittedCount = 0;
+        Map<String, Integer> problemTypeMap = new LinkedHashMap<>();
+        Map<String, int[]> districtStatMap = new LinkedHashMap<>();
+
+        for (EquipmentBaseOutDTO out : deviceList) {
+            // 设备类型名称
+            String typeName = typeNameMap.get(out.getEquipmentTypeId());
+            out.setEquipmentTypeName(typeName);
+
+            // 设备运维状态
+            EquipmentStatus equipmentStatus = equipmentStatusMap.get(out.getEquipmentId());
+            if (equipmentStatus != null) {
+                out.setEquipmentStatus(BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class));
+            }
+            // 是否有未办结工单
+            out.setWorkOrderSubmitted(deviceWithActiveOrder.contains(out.getEquipmentId()));
+
+            // 最新监测数据
+            ManholeData latest = null;
+            List<ManholeData> dataList = imeiDataMap.get(out.getEquipmentCode());
+            if (CollUtil.isNotEmpty(dataList)) {
+                latest = dataList.get(0);
+                out.setManholeData(BeanUtil.copyProperties(latest, ManholeDataOutDTO.class));
+            }
+
+            // 异常判定
+            List<String> faultTypes = judgeFaultTypes(latest, equipmentStatus);
+            boolean isAbnormal = CollUtil.isNotEmpty(faultTypes);
+            if (isAbnormal) {
+                abnormalCount++;
+                if (Boolean.TRUE.equals(out.getWorkOrderSubmitted())) {
+                    submittedCount++;
+                }
+                // 问题类型分布
+                for (String faultType : faultTypes) {
+                    problemTypeMap.merge(faultType, 1, Integer::sum);
+                }
+                abnormalDeviceList.add(out);
+            }
+            // 区域排行
+            String district = StringUtils.isNotBlank(out.getDistrict()) ? out.getDistrict() : "未分配";
+            int[] stat = districtStatMap.computeIfAbsent(district, k -> new int[2]);
+            stat[0]++;
+            if (isAbnormal) {
+                stat[1]++;
+            }
+        }
+
+        // ============ 7. KPI 指标 ============
+        Map<String, Object> kpi = new LinkedHashMap<>();
+        kpi.put("totalTerminal", equipmentList.size());
+        kpi.put("abnormalTerminal", abnormalCount);
+        kpi.put("abnormalRate", equipmentList.size() > 0 ? round1(abnormalCount * 100.0 / equipmentList.size()) : 0);
+        kpi.put("submittedCount", submittedCount);
+        kpi.put("solvedRate", abnormalCount > 0 ? round1(submittedCount * 100.0 / abnormalCount) : 0);
+        // 工单统计(日期范围内)
+        List<WorkOrder> rangeOrders = workOrderMapper.selectList(
+                new LambdaQueryWrapper<WorkOrder>()
+                        .ge(WorkOrder::getCreateTime, startDate)
+                        .le(WorkOrder::getCreateTime, endDate));
+        int totalCase = rangeOrders.size();
+        long finishedCase = rangeOrders.stream().filter(o -> Integer.valueOf(6).equals(o.getOrderStatus())).count();
+        long delayCase = rangeOrders.stream().filter(o -> Integer.valueOf(8).equals(o.getOrderStatus()) || Integer.valueOf(9).equals(o.getOrderStatus())).count();
+        kpi.put("totalCase", totalCase);
+        kpi.put("disposalRate", totalCase > 0 ? round1(finishedCase * 100.0 / totalCase) : 0);
+        kpi.put("delayRate", totalCase > 0 ? round1(delayCase * 100.0 / totalCase) : 0);
+        // 平均处置时长(已结案工单:实际完成时间-派单时间,小时)
+        kpi.put("avgDisposalTime", calcAvgDisposalTime(rangeOrders));
+        outDTO.setKpi(kpi);
+
+        // ============ 8. 异常终端趋势(基于 jg_device_data 历史记录) ============
+        outDTO.setAbnormalTerminalTrend(buildAbnormalTerminalTrend(equipmentCodeList, startDate, endDate, trendType));
+
+        // ============ 9. 处置率/延期率趋势(基于 work_order) ============
+        outDTO.setCaseRateTrend(buildCaseRateTrend(rangeOrders, startDate, endDate, trendType));
+
+        // ============ 10. 问题类型分布 ============
+        List<Map<String, Object>> problemTypeDistribution = new ArrayList<>();
+        for (Map.Entry<String, Integer> entry : problemTypeMap.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("name", entry.getKey());
+            item.put("value", entry.getValue());
+            problemTypeDistribution.add(item);
+        }
+        outDTO.setProblemTypeDistribution(problemTypeDistribution);
+
+        // ============ 11. 区域排行 ============
+        List<Map<String, Object>> regionRank = new ArrayList<>();
+        for (Map.Entry<String, int[]> entry : districtStatMap.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("name", entry.getKey());
+            item.put("total", entry.getValue()[0]);
+            item.put("abnormal", entry.getValue()[1]);
+            regionRank.add(item);
+        }
+        regionRank.sort((a, b) -> ((Integer) b.get("abnormal")) - ((Integer) a.get("abnormal")));
+        outDTO.setRegionRank(regionRank);
+
+        // ============ 12. 异常终端列表 ============
+        outDTO.setAbnormalDevices(abnormalDeviceList);
+        return outDTO;
+    }
+
+    /**
+     * 异常判定:倾斜超标/水位超限/水浸报警/通讯故障/电池低电量(<=15)/信号弱(<=20)
+     * 注意:equipmentStatus 为 null 时不判定通讯故障(用于历史记录趋势统计,无法获知当时在线状态)
+     */
+    private List<String> judgeFaultTypes(ManholeData data, EquipmentStatus equipmentStatus) {
+        List<String> faultTypes = new ArrayList<>();
+        boolean hasStatus = equipmentStatus != null;
+        boolean online = hasStatus && Integer.valueOf(1).equals(equipmentStatus.getOnlineStatus());
+        if (data == null) {
+            // 无监测数据:有状态记录且不在线视为通讯故障,无状态记录也视为通讯故障
+            if (!hasStatus || !online) {
+                faultTypes.add("通讯故障");
+            }
+            return faultTypes;
+        }
+        if (hasStatus && !online) {
+            faultTypes.add("通讯故障");
+        }
+        // 倾斜超标
+        if (isOverThreshold(data.getTiltAngle(), data.getAngleAlarmThreshold())) {
+            faultTypes.add("倾斜超标");
+        }
+        // 水位超限
+        if ("1".equals(data.getWaterLevelAlarmStatus())) {
+            faultTypes.add("水位超限");
+        }
+        // 水浸报警
+        if ("1".equals(data.getWaterInfiltrationAlarmStatus())) {
+            faultTypes.add("水浸报警");
+        }
+        // 电池低电量(<=15%)
+        if (isNumberNotAbove(data.getBatteryLevel(), 15)) {
+            faultTypes.add("电池低电量");
+        }
+        // 信号弱(<=20)
+        if (isNumberNotAbove(data.getSignalStrength(), 20)) {
+            faultTypes.add("信号弱");
+        }
+        return faultTypes;
+    }
+
+    /**
+     * 数值字段超过阈值判定
+     */
+    private boolean isOverThreshold(String value, String threshold) {
+        if (StringUtils.isBlank(value) || StringUtils.isBlank(threshold)) {
+            return false;
+        }
+        try {
+            return Double.parseDouble(value.trim()) > Double.parseDouble(threshold.trim());
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+    }
+
+    /**
+     * 数值字段不大于阈值判定
+     */
+    private boolean isNumberNotAbove(String value, double maxValue) {
+        if (StringUtils.isBlank(value)) {
+            return false;
+        }
+        try {
+            double v = Double.parseDouble(value.trim());
+            return v > 0 && v <= maxValue;
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+    }
+
+    /**
+     * 平均处置时长:已结案工单 (realFinishTime - dispatchTime) 平均小时
+     */
+    private double calcAvgDisposalTime(List<WorkOrder> orders) {
+        if (CollUtil.isEmpty(orders)) {
+            return 0;
+        }
+        List<WorkOrder> finishedOrders = orders.stream()
+                .filter(o -> Integer.valueOf(6).equals(o.getOrderStatus()))
+                .filter(o -> o.getRealFinishTime() != null && o.getDispatchTime() != null)
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(finishedOrders)) {
+            return 0;
+        }
+        double totalHours = 0;
+        for (WorkOrder order : finishedOrders) {
+            long diffMillis = order.getRealFinishTime().getTime() - order.getDispatchTime().getTime();
+            totalHours += diffMillis / (1000.0 * 60 * 60);
+        }
+        return round1(totalHours / finishedOrders.size());
+    }
+
+    /**
+     * 异常终端趋势:按周度/月度分组统计 jg_device_data 历史记录中的异常设备去重数
+     */
+    private List<Map<String, Object>> buildAbnormalTerminalTrend(List<String> equipmentCodeList,
+                                                                 Date startDate, Date endDate, int trendType) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (CollUtil.isEmpty(equipmentCodeList)) {
+            return result;
+        }
+        List<ManholeData> historyDataList = manholeDataMapper.selectList(
+                new LambdaQueryWrapper<ManholeData>()
+                        .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                        .ge(ManholeData::getCreateTime, startDate)
+                        .le(ManholeData::getCreateTime, endDate)
+                        .orderByAsc(ManholeData::getCreateTime));
+        if (CollUtil.isEmpty(historyDataList)) {
+            return result;
+        }
+        Map<String, Set<String>> periodAbnormalDevices = new LinkedHashMap<>();
+        Map<String, Set<String>> periodAllDevices = new LinkedHashMap<>();
+        for (ManholeData data : historyDataList) {
+            String periodKey = formatPeriodKey(data.getCreateTime(), trendType);
+            if (periodKey == null) {
+                continue;
+            }
+            periodAllDevices.computeIfAbsent(periodKey, k -> new HashSet<>()).add(data.getImeiCardNumber());
+            if (CollUtil.isNotEmpty(judgeFaultTypes(data, null))) {
+                periodAbnormalDevices.computeIfAbsent(periodKey, k -> new HashSet<>()).add(data.getImeiCardNumber());
+            }
+        }
+        for (Map.Entry<String, Set<String>> entry : periodAbnormalDevices.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("trendLabel", entry.getKey());
+            item.put("abnormalTerminal", entry.getValue().size());
+            item.put("totalTerminal", periodAllDevices.getOrDefault(entry.getKey(), new HashSet<>()).size());
+            result.add(item);
+        }
+        result.sort(Comparator.comparing(m -> String.valueOf(m.get("trendLabel"))));
+        return result;
+    }
+
+    /**
+     * 处置率/延期率趋势:按周度/月度分组统计工单
+     */
+    private List<Map<String, Object>> buildCaseRateTrend(List<WorkOrder> orders,
+                                                         Date startDate, Date endDate, int trendType) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (CollUtil.isEmpty(orders)) {
+            return result;
+        }
+        Map<String, List<WorkOrder>> periodGroup = new LinkedHashMap<>();
+        for (WorkOrder order : orders) {
+            String periodKey = formatPeriodKey(order.getCreateTime(), trendType);
+            if (periodKey != null) {
+                periodGroup.computeIfAbsent(periodKey, k -> new ArrayList<>()).add(order);
+            }
+        }
+        for (Map.Entry<String, List<WorkOrder>> entry : periodGroup.entrySet()) {
+            List<WorkOrder> periodOrders = entry.getValue();
+            long finished = periodOrders.stream().filter(o -> Integer.valueOf(6).equals(o.getOrderStatus())).count();
+            long delay = periodOrders.stream().filter(o -> Integer.valueOf(8).equals(o.getOrderStatus()) || Integer.valueOf(9).equals(o.getOrderStatus())).count();
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("trendLabel", entry.getKey());
+            item.put("caseDisposalRate", periodOrders.size() > 0 ? round1(finished * 100.0 / periodOrders.size()) : 0);
+            item.put("caseDelayRate", periodOrders.size() > 0 ? round1(delay * 100.0 / periodOrders.size()) : 0);
+            result.add(item);
+        }
+        result.sort(Comparator.comparing(m -> String.valueOf(m.get("trendLabel"))));
+        return result;
+    }
+
+    /**
+     * 周期标签:周度 yyyy-Www / 月度 yyyy-MM
+     */
+    private String formatPeriodKey(Date date, int trendType) {
+        if (date == null) {
+            return null;
+        }
+        LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        if (trendType == 1) {
+            WeekFields weekFields = WeekFields.of(Locale.getDefault());
+            int weekYear = localDate.get(weekFields.weekBasedYear());
+            int weekNum = localDate.get(weekFields.weekOfWeekBasedYear());
+            return String.format("%d-W%02d", weekYear, weekNum);
+        }
+        return localDate.format(DateTimeFormatter.ofPattern("yyyy-MM"));
+    }
+
+    /**
+     * 设备类型ID列表(避免空集合导致SQL错误)
+     */
+    private List<String> equipmentTypeIdsOf(List<EquipmentBaseOutDTO> deviceList) {
+        List<String> typeIds = deviceList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId)
+                .filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
+        return typeIds.isEmpty() ? Collections.singletonList("-1") : typeIds;
+    }
+
+    /**
+     * 保留一位小数
+     */
+    private double round1(double value) {
+        return Math.round(value * 10) / 10.0;
+    }
+
+    /**
+     * 空数据KPI
+     */
+    private Map<String, Object> buildEmptyKpi() {
+        Map<String, Object> kpi = new LinkedHashMap<>();
+        kpi.put("totalTerminal", 0);
+        kpi.put("abnormalTerminal", 0);
+        kpi.put("abnormalRate", 0);
+        kpi.put("submittedCount", 0);
+        kpi.put("solvedRate", 0);
+        kpi.put("totalCase", 0);
+        kpi.put("disposalRate", 0);
+        kpi.put("delayRate", 0);
+        kpi.put("avgDisposalTime", 0);
+        return kpi;
+    }
+
     /**
      * 查询统计数据列表(公共方法)
      * @param inDTO
@@ -172,7 +601,3 @@ public class MaintenanceDataStatServiceImpl extends ServiceImpl<MaintenanceDataS
         return groupedMap;
     }
 }
-
-
-
-

+ 235 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/ManholeLayerServiceImpl.java

@@ -0,0 +1,235 @@
+package com.zksy.base.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.service.AlarmDataService;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentStatus;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.manhole.domain.ManholeData;
+import com.zksy.base.manhole.service.ManholeDataService;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.base.service.EquipmentStatusService;
+import com.zksy.base.service.EquipmentTypeService;
+import com.zksy.base.service.ManholeLayerService;
+import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
+import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
+import com.zksy.manhole.dto.out.ManholeLayerStatisticsOutDTO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 态势图层 - 窨井设备服务实现
+ */
+@Service
+public class ManholeLayerServiceImpl implements ManholeLayerService {
+
+    @Autowired
+    private EquipmentBaseService equipmentBaseService;
+
+    @Autowired
+    private EquipmentStatusService equipmentStatusService;
+
+    @Autowired
+    private EquipmentTypeService equipmentTypeService;
+
+    @Autowired
+    private ManholeDataService manholeDataService;
+
+    @Autowired
+    private AlarmDataService alarmDataService;
+
+    /**
+     * 获取态势图层数据(设备列表 + 统计)
+     */
+    @Override
+    public Map<String, Object> getManholeLayerData(ManholeDeviceListInDTO listInDTO) {
+        // 1. 仅查窨井类型设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10)
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+
+        // 2. 构建设备基础信息查询条件(不按状态预过滤,前端做前端过滤)
+        LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .and(StrUtil.isNotBlank(listInDTO.getKeyword()), wrapper -> wrapper
+                        .like(EquipmentBase::getEquipmentCode, listInDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentName, listInDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentLocation, listInDTO.getKeyword()))
+                .orderByDesc(EquipmentBase::getUpdateTime);
+
+        List<EquipmentBase> deviceList = equipmentBaseService.list(queryWrapper);
+        if (CollUtil.isEmpty(deviceList)) {
+            Map<String, Object> result = new HashMap<>();
+            result.put("devices", Collections.emptyList());
+            ManholeLayerStatisticsOutDTO stats = new ManholeLayerStatisticsOutDTO();
+            stats.setTotal(0);
+            result.put("statistics", stats);
+            return result;
+        }
+
+        // 4. 查询所有设备的状态信息(LEFT JOIN 模式:即使设备状态表无记录也要保留设备)
+        List<String> equipmentIdList = deviceList.stream()
+                .map(EquipmentBase::getEquipmentId)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<String, EquipmentStatus> statusMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentIdList)) {
+            // 按 equipment_id 查询状态(非主键 status_id),确保 LEFT JOIN 语义
+            List<EquipmentStatus> allStatusList = equipmentStatusService.list(
+                    new LambdaQueryWrapper<EquipmentStatus>()
+                            .in(EquipmentStatus::getEquipmentId, equipmentIdList));
+            if (CollUtil.isNotEmpty(allStatusList)) {
+                statusMap.putAll(allStatusList.stream().collect(
+                        Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (key1, key2) -> key2)));
+            }
+        }
+
+        // 5. 查询设备类型名称
+        List<String> equipmentTypeIdList = deviceList.stream()
+                .map(EquipmentBase::getEquipmentTypeId)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<String, EquipmentType> typeMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentTypeIdList)) {
+            List<EquipmentType> typeList = equipmentTypeService.list(new LambdaQueryWrapper<EquipmentType>()
+                    .in(EquipmentType::getId, equipmentTypeIdList));
+            if (CollUtil.isNotEmpty(typeList)) {
+                typeMap = typeList.stream().collect(
+                        Collectors.toMap(EquipmentType::getId, Function.identity(), (key1, key2) -> key2));
+            }
+        }
+
+        // 6. 查询井盖监测数据(最新一条)
+        Map<String, List<ManholeData>> imeiCardNumberMapList = new HashMap<>();
+        if (Boolean.TRUE.equals(listInDTO.getIsQueryManholeData())) {
+            List<String> equipmentCodeList = deviceList.stream()
+                    .map(EquipmentBase::getEquipmentCode)
+                    .distinct()
+                    .collect(Collectors.toList());
+            List<ManholeData> manholeDataList = manholeDataService.list(new LambdaQueryWrapper<ManholeData>()
+                    .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                    .orderByDesc(ManholeData::getCreateTime));
+            imeiCardNumberMapList = manholeDataList.stream()
+                    .collect(Collectors.groupingBy(ManholeData::getImeiCardNumber));
+        }
+
+        // 7. 查询报警数据(最新一条)
+        Map<String, List<AlarmData>> alarmDataMap = new HashMap<>();
+        // 有状态的才查报警数据
+        List<String> statusEquipmentIds = new ArrayList<>(statusMap.keySet());
+        if (CollUtil.isNotEmpty(statusEquipmentIds)) {
+            List<String> equipmentCodes = deviceList.stream()
+                    .filter(d -> statusMap.containsKey(d.getEquipmentId()))
+                    .map(EquipmentBase::getEquipmentCode)
+                    .distinct()
+                    .collect(Collectors.toList());
+            List<AlarmData> alarmDataList = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
+                    .in(AlarmData::getDeviceCode, equipmentCodes)
+                    .orderByDesc(AlarmData::getAlarmTime));
+            alarmDataMap = alarmDataList.stream()
+                    .collect(Collectors.groupingBy(AlarmData::getDeviceCode));
+        }
+
+        // 8. 组装出参
+        List<EquipmentBaseOutDTO> outDTOList = new ArrayList<>();
+        Map<String, Object> statisticsMap = new HashMap<>();
+        
+        int total = 0;
+        int inUse = 0;
+        int maintenance = 0;
+        int alarm = 0;
+        int normal = 0;
+        int online = 0;
+        int offline = 0;
+
+        for (EquipmentBase device : deviceList) {
+            EquipmentBaseOutDTO outDTO = new EquipmentBaseOutDTO();
+            BeanUtils.copyProperties(device, outDTO);
+
+            // 设备类型名称
+            EquipmentType equipmentType = typeMap.get(device.getEquipmentTypeId());
+            if (equipmentType != null) {
+                outDTO.setEquipmentTypeName(equipmentType.getTypeName());
+            }
+
+            // 设备状态(可能存在但为空的情况)
+            EquipmentStatus status = statusMap.get(device.getEquipmentId());
+            if (status != null) {
+                // 转换 DTO - EquipmentStatusOutDTO 只有 4 个字段
+                com.zksy.manhole.dto.out.EquipmentStatusOutDTO outStatus = new com.zksy.manhole.dto.out.EquipmentStatusOutDTO();
+                outStatus.setCurrentStatus(status.getCurrentStatus());
+                outStatus.setAlarmStatus(status.getAlarmStatus());
+                outStatus.setOnlineStatus(status.getOnlineStatus());
+                // statusUpdateTime 转换为 Date(使用 Hutool)
+                outStatus.setStatusUpdateTime(DateUtil.date(status.getStatusUpdateTime()));
+                outDTO.setEquipmentStatus(outStatus);
+                
+                // 统计:运维状态(精确匹配,不 fallback)
+                if (Objects.equals(status.getCurrentStatus(), 1)) inUse++;
+                else if (Objects.equals(status.getCurrentStatus(), 3)) maintenance++;
+                
+                // 统计:报警状态(alarmStatus=0 才算 normal)
+                if (Objects.equals(status.getAlarmStatus(), 1)) alarm++;
+                else if (Objects.equals(status.getAlarmStatus(), 0)) normal++;
+                
+                // 统计:在线状态(onlineStatus=0 才算 offline)
+                if (Objects.equals(status.getOnlineStatus(), 1)) online++;
+                else if (Objects.equals(status.getOnlineStatus(), 0)) offline++;
+            } else {
+                // 没有状态记录的设备视为离线(不纳入报警/正常统计)
+                outDTO.setEquipmentStatus(null);
+                offline++;
+            }
+
+            // 坐标信息(来自设备基础表)
+            outDTO.setLongitude(device.getLongitude());
+            outDTO.setLatitude(device.getLatitude());
+
+            // 井盖监测数据
+            if (Boolean.TRUE.equals(listInDTO.getIsQueryManholeData())) {
+                List<ManholeData> dataList = imeiCardNumberMapList.get(device.getEquipmentCode());
+                if (CollUtil.isNotEmpty(dataList)) {
+                    ManholeData latestData = dataList.get(0);
+                    com.zksy.manhole.dto.out.ManholeDataOutDTO manholeDataOutDTO = new com.zksy.manhole.dto.out.ManholeDataOutDTO();
+                    BeanUtils.copyProperties(latestData, manholeDataOutDTO);
+                    outDTO.setManholeData(manholeDataOutDTO);
+                }
+            }
+
+            // 报警数据(最新一条)
+            List<AlarmData> alarms = alarmDataMap.get(device.getEquipmentCode());
+            if (CollUtil.isNotEmpty(alarms)) {
+                AlarmData latestAlarm = alarms.get(0);
+                outDTO.setAlarmData(latestAlarm);
+            }
+
+            outDTOList.add(outDTO);
+            total++;
+        }
+
+        statisticsMap.put("total", total);
+        statisticsMap.put("inUse", inUse);
+        statisticsMap.put("maintenance", maintenance);
+        statisticsMap.put("alarm", alarm);
+        statisticsMap.put("normal", normal);
+        statisticsMap.put("online", online);
+        statisticsMap.put("offline", offline);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("devices", outDTOList);
+        result.put("statistics", statisticsMap);
+        return result;
+    }
+
+
+
+}

+ 32 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/MonitoringServiceImpl.java

@@ -61,6 +61,7 @@ public class MonitoringServiceImpl implements MonitoringService {
                 .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
                 .like(StrUtil.isNotBlank(equipmentLocation), EquipmentBase::getEquipmentLocation, equipmentLocation)
                 .orderByDesc(EquipmentBase::getCreateTime);
+        constrainToMonitoringTypes(baseWrapper, "flow");
 
         Page<EquipmentBase> basePage = equipmentBaseMapper.selectPage(new Page<>(pageNum, pageSize), baseWrapper);
         Page<FlowMonitoringVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
@@ -200,6 +201,7 @@ public class MonitoringServiceImpl implements MonitoringService {
                 .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
                 .like(StrUtil.isNotBlank(equipmentLocation), EquipmentBase::getEquipmentLocation, equipmentLocation)
                 .orderByDesc(EquipmentBase::getCreateTime);
+        constrainToMonitoringTypes(baseWrapper, "pressure");
 
         Page<EquipmentBase> basePage = equipmentBaseMapper.selectPage(new Page<>(pageNum, pageSize), baseWrapper);
         Page<PressureMonitoringVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
@@ -282,6 +284,7 @@ public class MonitoringServiceImpl implements MonitoringService {
                 .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
                 .like(StrUtil.isNotBlank(equipmentLocation), EquipmentBase::getEquipmentLocation, equipmentLocation)
                 .orderByDesc(EquipmentBase::getCreateTime);
+        constrainToMonitoringTypes(baseWrapper, "leakage");
 
         Page<EquipmentBase> basePage = equipmentBaseMapper.selectPage(new Page<>(pageNum, pageSize), baseWrapper);
         Page<LeakageMonitoringVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
@@ -454,4 +457,33 @@ public class MonitoringServiceImpl implements MonitoringService {
         }
         return result;
     }
+
+    /** Restrict the equipment query before pagination so each monitoring page only contains its sub-devices. */
+    private void constrainToMonitoringTypes(LambdaQueryWrapper<EquipmentBase> wrapper, String monitorKind) {
+        String[] keywords;
+        switch (monitorKind) {
+            case "pressure":
+                keywords = new String[]{"压力", "pressure", "消防"};
+                break;
+            case "leakage":
+                keywords = new String[]{"漏失", "漏损", "泄漏", "噪声", "音频", "noise"};
+                break;
+            default:
+                keywords = new String[]{"流量", "flow", "雷达", "遥测", "telemetry"};
+                break;
+        }
+        List<EquipmentType> types = equipmentTypeMapper.selectList(new LambdaQueryWrapper<EquipmentType>());
+        Set<String> ids = types.stream()
+                .filter(type -> {
+                    String value = ((type.getTypeId() == null ? "" : type.getTypeId()) + " "
+                            + (type.getTypeName() == null ? "" : type.getTypeName())).toLowerCase(Locale.ROOT);
+                    return Arrays.stream(keywords).anyMatch(keyword -> value.contains(keyword.toLowerCase(Locale.ROOT)));
+                })
+                .map(EquipmentType::getId)
+                .filter(StrUtil::isNotBlank)
+                .collect(Collectors.toSet());
+        // Keep an empty result portable across database drivers that reject IN ().
+        wrapper.in(EquipmentBase::getEquipmentTypeId,
+                ids.isEmpty() ? Collections.singleton("__NO_MONITORING_TYPE__") : ids);
+    }
 }

+ 7 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImpl.java

@@ -375,7 +375,13 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         }
 
         int total = allDevices.size();
-        int fromIndex = (int) ((pageNum - 1) * pageSize);
+        long offset = (pageNum - 1) * pageSize;
+        if (pageNum < 1 || pageSize < 1 || offset >= total) {
+            Page<AbnormalDeviceVO> emptyPage = new Page<>(pageNum, pageSize, total);
+            emptyPage.setRecords(new ArrayList<>());
+            return emptyPage;
+        }
+        int fromIndex = (int) offset;
         int toIndex = Math.min(fromIndex + (int) pageSize, total);
         List<EquipmentBase> pageDevices = allDevices.subList(fromIndex, toIndex);
 

+ 120 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WorkOrderServiceImpl.java

@@ -8,6 +8,8 @@ import com.zksy.base.domain.CaseInfo;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.warning.domain.EarlyWarning;
+import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.base.mapper.CaseInfoMapper;
 import com.zksy.base.mapper.EquipmentBaseMapper;
 import com.zksy.base.mapper.WorkOrderLogMapper;
@@ -27,10 +29,12 @@ import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Random;
+import java.util.stream.Collectors;
 
 /**
 * @author Administrator
@@ -54,17 +58,46 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     @Autowired
     private EquipmentBaseMapper equipmentBaseMapper;
 
+    @Autowired
+    private EarlyWarningService earlyWarningService;
+
+    /**
+     * 查询窨井类型设备ID列表(equipment_type_id: 9=排水窨井, 10=供水窨井)
+     */
+    private List<String> getManholeDeviceIds() {
+        LambdaQueryWrapper<EquipmentBase> equipmentQuery = new LambdaQueryWrapper<>();
+        equipmentQuery.in(EquipmentBase::getEquipmentTypeId, Arrays.asList("5", "9", "10"));
+        return equipmentBaseMapper.selectList(equipmentQuery).stream()
+                .map(EquipmentBase::getEquipmentId)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 构造空分页结果
+     */
+    private Page<WorkOrderOutDTO> emptyPage(long pageNum, long pageSize) {
+        Page<WorkOrderOutDTO> emptyPage = new Page<>(pageNum, pageSize, 0);
+        emptyPage.setRecords(new ArrayList<>());
+        return emptyPage;
+    }
+
     /**
-     * 分页查询工单信息列表
+     * 分页查询工单信息列表(仅窨井类型设备)
      * @param pageInDTO
      * @return
      */
     @Override
     public Page<WorkOrderOutDTO> findByPage(WorkOrderPageInDTO pageInDTO) {
         log.info("分页查询工单信息列表-入参:{}", pageInDTO);
+        List<String> manholeDeviceIds = getManholeDeviceIds();
+        if (manholeDeviceIds.isEmpty()) {
+            return emptyPage(pageInDTO.getPageNum(), pageInDTO.getPageSize());
+        }
         Page<WorkOrder> page = new Page<>(pageInDTO.getPageNum(), pageInDTO.getPageSize());
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.like(StringUtils.isNotEmpty(pageInDTO.getOrderNo()), WorkOrder::getOrderNo, pageInDTO.getOrderNo())
+        queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
+                .eq(StringUtils.isNotEmpty(pageInDTO.getDeviceId()), WorkOrder::getDeviceId, pageInDTO.getDeviceId())
+                .like(StringUtils.isNotEmpty(pageInDTO.getOrderNo()), WorkOrder::getOrderNo, pageInDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(pageInDTO.getCaseNo()), WorkOrder::getCaseNo, pageInDTO.getCaseNo())
                 .orderByDesc(WorkOrder::getUpdateTime);
         Page<WorkOrder> rs = this.page(page, queryWrapper);
@@ -77,15 +110,28 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     /**
      * 新增工单
      * @param inDTO
-     * @return
+     * @return 工单ID(新增成功返回 orderId,失败返回 null)
      */
     @Override
-    public boolean addWorkOrder(WorkOrderAddInDTO inDTO) {
+    public Long addWorkOrder(WorkOrderAddInDTO inDTO) {
         log.info("新增工单-入参:{}", inDTO);
+        resolveWarningDevice(inDTO);
+        // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
+        if (inDTO.getDeviceId() != null) {
+            Long count = this.lambdaQuery()
+                    .eq(WorkOrder::getDeviceId, inDTO.getDeviceId())
+                    .notIn(WorkOrder::getOrderStatus, 6, 7)
+                    .count();
+            if (count > 0) {
+                throw new ServiceException("该设备已有处理中的工单,需结案或驳回后才能创建新工单");
+            }
+        }
         WorkOrder workOrder = BeanUtil.copyProperties(inDTO, WorkOrder.class);
         workOrder.setOrderNo(generateOrderNo());
         // 初始状态:1-待派单
         workOrder.setOrderStatus(1);
+        workOrder.setCreateTime(new Date());
+        workOrder.setUpdateTime(new Date());
         if(inDTO.getCaseId() != null){
             CaseInfo caseInfo = caseInfoMapper.selectById(inDTO.getCaseId());
             if(caseInfo != null){
@@ -98,6 +144,13 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
                 workOrder.setDeviceCode(equipmentBase.getEquipmentCode());
             }
         }
+        // 设置维修前后图片(如果 DTO 中有传递)
+        if (inDTO.getMaintenanceBeforeImg() != null) {
+            workOrder.setMaintenanceBeforeImg(inDTO.getMaintenanceBeforeImg());
+        }
+        if (inDTO.getMaintenanceAfterImg() != null) {
+            workOrder.setMaintenanceAfterImg(inDTO.getMaintenanceAfterImg());
+        }
         this.save(workOrder);
         WorkOrderLog workOrderLog = new WorkOrderLog();
         workOrderLog.setOrderId(workOrder.getOrderId());
@@ -109,7 +162,34 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
             workOrderLog.setOperUserId(loginUser.getUserId());
             workOrderLog.setOperUserName(loginUser.getUser().getNickName());
         }
-        return workOrderLogMapper.insert(workOrderLog) > 0;
+        workOrderLogMapper.insert(workOrderLog);
+        // 返回工单ID,供后续派单使用
+        return workOrder.getOrderId();
+    }
+
+    /**
+     * 确认预警时前端不再重复选择设备,按预警已保存的设备编码回填工单设备信息。
+     */
+    private void resolveWarningDevice(WorkOrderAddInDTO inDTO) {
+        if (inDTO == null || StringUtils.isBlank(inDTO.getAlarmId())) {
+            return;
+        }
+        EarlyWarning warning = earlyWarningService.getById(inDTO.getAlarmId());
+        if (warning == null) {
+            return;
+        }
+        if (StringUtils.isBlank(inDTO.getDeviceCode())) {
+            inDTO.setDeviceCode(warning.getDeviceCode());
+        }
+        if (StringUtils.isNotBlank(inDTO.getDeviceId()) || StringUtils.isBlank(inDTO.getDeviceCode())) {
+            return;
+        }
+        EquipmentBase equipment = equipmentBaseMapper.selectOne(new LambdaQueryWrapper<EquipmentBase>()
+                .eq(EquipmentBase::getEquipmentCode, inDTO.getDeviceCode())
+                .last("LIMIT 1"));
+        if (equipment != null) {
+            inDTO.setDeviceId(equipment.getEquipmentId());
+        }
     }
 
     /**
@@ -126,18 +206,23 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     }
 
     /**
-     * 经办工单分页查询(处理中:待接单、已接单、处理中、待验收)
+     * 经办工单分页查询(处理中:待接单、已接单、处理中、待验收,仅窨井类型设备
      * @param inDTO
      * @return
      */
     @Override
     public Page<WorkOrderOutDTO> findProcessingPage(WorkOrderProcessingInDTO inDTO) {
         log.info("经办工单分页查询-入参:{}", inDTO);
+        List<String> manholeDeviceIds = getManholeDeviceIds();
+        if (manholeDeviceIds.isEmpty()) {
+            return emptyPage(inDTO.getPageNum(), inDTO.getPageSize());
+        }
         Page<WorkOrder> page = new Page<>(inDTO.getPageNum(), inDTO.getPageSize());
         // 经办工单状态:2-待接单 3-已接单 4-处理中 5-待验收 8-延期审核
         List<Integer> processingStatusList = Arrays.asList(2, 3, 4, 5, 8);
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.in(WorkOrder::getOrderStatus, processingStatusList)
+        queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
+                .in(WorkOrder::getOrderStatus, processingStatusList)
                 .like(StringUtils.isNotEmpty(inDTO.getOrderNo()), WorkOrder::getOrderNo, inDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(inDTO.getCaseNo()), WorkOrder::getCaseNo, inDTO.getCaseNo())
                 .eq(inDTO.getOrderType() != null, WorkOrder::getOrderType, inDTO.getOrderType())
@@ -152,18 +237,23 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     }
 
     /**
-     * 办结工单分页查询(已办结:已结案、已驳回)
+     * 办结工单分页查询(已办结:已结案、已驳回,仅窨井类型设备
      * @param inDTO
      * @return
      */
     @Override
     public Page<WorkOrderOutDTO> findFinishedPage(WorkOrderFinishedInDTO inDTO) {
         log.info("办结工单分页查询-入参:{}", inDTO);
+        List<String> manholeDeviceIds = getManholeDeviceIds();
+        if (manholeDeviceIds.isEmpty()) {
+            return emptyPage(inDTO.getPageNum(), inDTO.getPageSize());
+        }
         Page<WorkOrder> page = new Page<>(inDTO.getPageNum(), inDTO.getPageSize());
         // 办结工单状态:6-已结案 7-已驳回 9-已延期
         List<Integer> finishedStatusList = Arrays.asList(6, 7, 9);
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.in(WorkOrder::getOrderStatus, finishedStatusList)
+        queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
+                .in(WorkOrder::getOrderStatus, finishedStatusList)
                 .like(StringUtils.isNotEmpty(inDTO.getOrderNo()), WorkOrder::getOrderNo, inDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(inDTO.getCaseNo()), WorkOrder::getCaseNo, inDTO.getCaseNo())
                 .eq(inDTO.getOrderType() != null, WorkOrder::getOrderType, inDTO.getOrderType())
@@ -213,10 +303,16 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         } else if(inDTO.getOperType() == 4){
             //4-开始维修
             entity.setOrderStatus(4);
+            if (StringUtils.isNotBlank(inDTO.getMaintenanceBeforeImg())) {
+                entity.setMaintenanceBeforeImg(inDTO.getMaintenanceBeforeImg());
+            }
         } else if(inDTO.getOperType() == 5){
             //5-提交验收
             entity.setOrderStatus(5);
             entity.setVerifyUser(workOrder.getDispatchUser());
+            if (StringUtils.isNotBlank(inDTO.getMaintenanceAfterImg())) {
+                entity.setMaintenanceAfterImg(inDTO.getMaintenanceAfterImg());
+            }
         } else if(inDTO.getOperType() == 6){
             //6-验收通过
             entity.setOrderStatus(6);
@@ -233,6 +329,21 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
                     caseInfoMapper.updateById(caseEntity);
                 }
             }
+            // 若工单关联了预警(alarmId 对应 early_warning 记录),验收通过时自动解除预警
+            if(workOrder.getAlarmId() != null){
+                try {
+                    EarlyWarning warning = earlyWarningService.getById(workOrder.getAlarmId());
+                    if(warning != null){
+                        earlyWarningService.resolveWarning(warning.getWarningId(),
+                                "工单验收通过(" + workOrder.getOrderNo() + "),预警已解除");
+                        log.info("工单验收通过,自动解除预警: orderNo={}, warningId={}",
+                                workOrder.getOrderNo(), workOrder.getAlarmId());
+                    }
+                } catch (Exception e) {
+                    log.warn("工单验收通过后解除预警失败: orderNo={}, warningId={}",
+                            workOrder.getOrderNo(), workOrder.getAlarmId(), e);
+                }
+            }
         } else if(inDTO.getOperType() == 7){
             //7-驳回工单
             entity.setOrderStatus(7);

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

@@ -43,6 +43,15 @@ public class EarlyWarning implements Serializable {
     @ApiModelProperty("预警专项")
     private String warningSpecial;
 
+
+    @TableField("device_code")
+    @ApiModelProperty("关联设备编码")
+    private String deviceCode;
+
+    @TableField("device_name")
+    @ApiModelProperty("关联设备名称")
+    private String deviceName;
+
     @TableField("location")
     @ApiModelProperty("预警位置")
     private String location;
@@ -107,6 +116,7 @@ public class EarlyWarning implements Serializable {
     @ApiModelProperty("备注")
     private String remark;
 
+
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;
 }

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

@@ -44,7 +44,6 @@ public class WarningArchive implements Serializable {
     @TableField("warning_level")
     @ApiModelProperty("预警级别")
     private String warningLevel;
-
     @TableField("location")
     @ApiModelProperty("预警位置")
     private String location;

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

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

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

@@ -0,0 +1,13 @@
+package com.zksy.base.warning.domain;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class WarningClearDTO {
+    @NotBlank
+    private String warningId;
+    @NotBlank
+    private String clearRemark;
+}

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

@@ -0,0 +1,13 @@
+package com.zksy.base.warning.domain;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class WarningConfirmDTO {
+    @NotBlank
+    private String warningId;
+    private String assignedUser;
+    private String confirmRemark;
+}

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

@@ -38,7 +38,6 @@ public class WarningItem implements Serializable {
     @TableField("warning_special")
     @ApiModelProperty("预警专项")
     private String warningSpecial;
-
     @TableField("industry")
     @ApiModelProperty("所属行业")
     private String industry;

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

@@ -0,0 +1,13 @@
+package com.zksy.base.warning.domain;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class WarningProcessDTO {
+    @NotBlank
+    private String warningId;
+    @NotBlank
+    private String processContent;
+}

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

@@ -38,7 +38,6 @@ public class WarningReason implements Serializable {
     @TableField("description")
     @ApiModelProperty("原因描述")
     private String description;
-
     @TableField("sort_order")
     @ApiModelProperty("排序")
     private Integer sortOrder;

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

@@ -0,0 +1,39 @@
+package com.zksy.base.warning.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName(value = "warning_supervision", schema = "app_user")
+public class WarningSupervision implements Serializable {
+    @TableId(value = "supervision_id", type = IdType.ASSIGN_UUID)
+    @ApiModelProperty("督办ID")
+    private String supervisionId;
+    @TableField("warning_id")
+    private String warningId;
+    @TableField("supervision_user")
+    private String supervisionUser;
+    @TableField("supervision_content")
+    private String supervisionContent;
+    @TableField("notification_type")
+    private String notificationType;
+    @TableField("notification_content")
+    private String notificationContent;
+    @TableField("attachment_url")
+    private String attachmentUrl;
+    @TableField("create_by")
+    private String createBy;
+    @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

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

@@ -44,7 +44,6 @@ public class WarningTodo implements Serializable {
     @TableField("task_name")
     @ApiModelProperty("任务名称")
     private String taskName;
-
     @TableField("create_time")
     @ApiModelProperty("创建时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

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

@@ -16,7 +16,7 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
 
     IPage<Map<String, Object>> selectWarningList(Page<?> page, @Param("warningName") String warningName, @Param("warningType") String warningType, @Param("warningLevel") String warningLevel, @Param("status") String status);
 
-    IPage<Map<String, Object>> searchWarnings(Page<?> page, @Param("warningName") String warningName, @Param("warningNo") String warningNo, @Param("warningType") String warningType, @Param("warningLevel") String warningLevel, @Param("status") String status, @Param("location") String location, @Param("publisher") String publisher, @Param("ownershipUnit") String ownershipUnit, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
+    IPage<Map<String, Object>> searchWarnings(Page<?> page, @Param("warningName") String warningName, @Param("warningNo") String warningNo, @Param("warningType") String warningType, @Param("warningLevel") String warningLevel, @Param("status") String status, @Param("location") String location, @Param("publisher") String publisher, @Param("ownershipUnit") String ownershipUnit, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("excludePublisher") String excludePublisher, @Param("deviceCode") String deviceCode, @Param("deviceName") String deviceName);
 
     IPage<Map<String, Object>> selectTodoList(Page<?> page, @Param("userId") String userId, @Param("todoType") String todoType, @Param("warningName") String warningName);
 

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

@@ -0,0 +1,13 @@
+package com.zksy.base.warning.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.base.warning.domain.WarningSupervision;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface WarningSupervisionMapper extends BaseMapper<WarningSupervision> {
+    List<WarningSupervision> selectByWarningId(@Param("warningId") String warningId);
+}

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

@@ -6,6 +6,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.base.warning.domain.EarlyWarning;
 import com.zksy.base.warning.domain.WarningArchive;
 import com.zksy.base.warning.domain.WarningDisposal;
+import com.zksy.base.warning.domain.WarningSupervision;
+import com.zksy.base.warning.domain.WarningAttachment;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.time.LocalDateTime;
@@ -16,7 +18,7 @@ public interface EarlyWarningService extends IService<EarlyWarning> {
 
     IPage<Map<String, Object>> getWarningList(Page<?> page, String warningName, String warningType, String warningLevel, String status);
 
-    IPage<Map<String, Object>> searchWarnings(Page<?> page, String warningName, String warningNo, String warningType, String warningLevel, String status, String location, String publisher, String ownershipUnit, LocalDateTime startTime, LocalDateTime endTime);
+    IPage<Map<String, Object>> searchWarnings(Page<?> page, String warningName, String warningNo, String warningType, String warningLevel, String status, String location, String publisher, String ownershipUnit, LocalDateTime startTime, LocalDateTime endTime, String excludePublisher, String deviceCode, String deviceName);
 
     IPage<Map<String, Object>> getTodoList(Page<?> page, String userName, String todoType, String warningName);
 
@@ -34,6 +36,14 @@ public interface EarlyWarningService extends IService<EarlyWarning> {
 
     boolean publishWarning(String warningId);
 
+    boolean confirmWarning(String warningId, String assignedUser, String confirmRemark);
+
+    boolean misreportWarning(String warningId, String reason);
+
+    boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files);
+
+    boolean clearWarning(String warningId, String clearRemark);
+
     boolean upgradeWarning(String warningId, String newLevel, String disposalContent);
 
     boolean resolveWarning(String warningId, String disposalContent);
@@ -58,6 +68,10 @@ public interface EarlyWarningService extends IService<EarlyWarning> {
 
     List<Map<String, Object>> getAttachments(String warningId);
 
+    WarningAttachment getAttachment(String attachmentId);
+
+    List<WarningSupervision> getSupervisionList(String warningId);
+
     Map<String, Object> getStatistics();
 
     Map<String, Object> getDashboardStatistics();

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

@@ -9,6 +9,7 @@ import com.zksy.base.warning.mapper.*;
 import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.common.core.domain.entity.SysDictData;
 import com.zksy.common.utils.DictUtils;
+import com.zksy.common.utils.SecurityUtils;
 import com.zksy.service.MinioFileStorageService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -34,6 +35,9 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Autowired
     private WarningTodoMapper warningTodoMapper;
 
+    @Autowired
+    private WarningSupervisionMapper warningSupervisionMapper;
+
     @Autowired
     private WarningArchiveMapper warningArchiveMapper;
     @Autowired
@@ -45,8 +49,8 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     }
 
     @Override
-    public IPage<Map<String, Object>> searchWarnings(Page<?> page, String warningName, String warningNo, String warningType, String warningLevel, String status, String location, String publisher, String ownershipUnit, LocalDateTime startTime, LocalDateTime endTime) {
-        return earlyWarningMapper.searchWarnings(page, warningName, warningNo, warningType, warningLevel, status, location, publisher, ownershipUnit, startTime, endTime);
+    public IPage<Map<String, Object>> searchWarnings(Page<?> page, String warningName, String warningNo, String warningType, String warningLevel, String status, String location, String publisher, String ownershipUnit, LocalDateTime startTime, LocalDateTime endTime, String excludePublisher, String deviceCode, String deviceName) {
+        return earlyWarningMapper.searchWarnings(page, warningName, warningNo, warningType, warningLevel, status, location, publisher, ownershipUnit, startTime, endTime, excludePublisher, deviceCode, deviceName);
     }
 
     @Override
@@ -77,6 +81,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         result.put("gisLocation", getGisLocation(basicInfo));
         result.put("disposalHistory", getDisposalHistory(warningId));
         result.put("attachmentList", getAttachmentList(warningId));
+        result.put("supervisionList", getSupervisionList(warningId));
         result.put("alarmList", getLinkedAlarms(warningId));
         result.put("electronicArchive", generateElectronicArchive(basicInfo));
 
@@ -123,7 +128,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         archive.setLatitude(warningInfo.get("latitude") != null ? ((Number) warningInfo.get("latitude")).doubleValue() : null);
         archive.setOwnershipUnit((String) warningInfo.get("ownership_unit"));
         archive.setPublisher((String) warningInfo.get("publisher"));
-        archive.setPublishTime(warningInfo.get("publish_time") instanceof LocalDateTime ? (LocalDateTime) warningInfo.get("publishTime") : null);
+        archive.setPublishTime(warningInfo.get("publish_time") instanceof LocalDateTime ? (LocalDateTime) warningInfo.get("publish_time") : null);
         archive.setHandler((String) warningInfo.get("handler"));
         archive.setWarningContent((String) warningInfo.get("warning_content"));
         archive.setArchiveContent((String) warningInfo.get("warning_content"));
@@ -156,7 +161,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
                 linkAlarms(warning.getWarningId(), alarmIds);
             }
             if (files != null && !files.isEmpty()) {
-                saveAttachments(warning.getWarningId(), files);
+              saveAttachments(warning.getWarningId(), null, "REPORT", files);
             }
         }
         return result;
@@ -166,15 +171,23 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
      * 保存附件
      */
     private void saveAttachments(String warningId, List<MultipartFile> files) {
+        saveAttachments(warningId, null, "REPORT", files);
+    }
+
+    private void saveAttachments(String warningId, String disposalId, String stage, List<MultipartFile> files) {
         try {
             for (MultipartFile file : files) {
+                validateAttachment(file);
                 String path = minioFileStorageService.uploadFile(file, "warning");
                 // 上传文件
-                String fileName = path.substring(path.lastIndexOf("/") + 1);
+                String fileName = file.getOriginalFilename();
+                if (fileName == null || fileName.trim().isEmpty()) {
+                    fileName = path.substring(path.lastIndexOf("/") + 1);
+                }
                 // 保存附件信息
                 WarningAttachment attachment = new WarningAttachment();
                 attachment.setWarningId(warningId);
-                attachment.setAttachmentName(fileName);
+                attachment.setAttachmentName(("PROCESS".equals(stage) ? "[处理反馈] " : "[预警报告] ") + fileName);
                 attachment.setAttachmentUrl(path);
                 // 部分 MIME(如 vnd.openxmlformats-officedocument.*)长度超过 64,写入前按 DB 列宽截断兜底
                 attachment.setAttachmentType(truncateAttachmentType(file.getContentType()));
@@ -197,7 +210,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
                 linkAlarms(warning.getWarningId(), alarmIds);
             }
             if (files != null && !files.isEmpty()) {
-                saveAttachments(warning.getWarningId(), files);
+                saveAttachments(warning.getWarningId(), null, "REPORT", files);
             }
         }
         return result;
@@ -240,6 +253,10 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         archiveWrapper.eq(WarningArchive::getWarningId, warningId);
         warningArchiveMapper.delete(archiveWrapper);
 
+        LambdaQueryWrapper<WarningSupervision> supervisionWrapper = new LambdaQueryWrapper<>();
+        supervisionWrapper.eq(WarningSupervision::getWarningId, warningId);
+        warningSupervisionMapper.delete(supervisionWrapper);
+
         // 删除主表记录
         return removeById(warningId);
     }
@@ -266,13 +283,112 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         boolean result = updateById(warning);
 
         if (result) {
-            addDisposalRecord(warningId, "RELEASE", null, null, warning.getHandler(), "系统发布预警信息");
-            createTodo(warningId, warning.getHandler(), "待办预警", "TODO");
+            String assignee = isBlank(warning.getHandler()) ? currentUser() : warning.getHandler();
+            warning.setHandler(assignee);
+            updateById(warning);
+            addDisposalRecord(warningId, "RELEASE", null, null, assignee, "系统发布预警信息");
+            createTodo(warningId, assignee, "待办预警", "TODO");
         }
 
         return result;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean confirmWarning(String warningId, String assignedUser, String confirmRemark) {
+        EarlyWarning warning = getById(warningId);
+        if (warning == null || "CLOSED".equals(warning.getStatus())) return false;
+        LocalDateTime now = LocalDateTime.now();
+        String operator = currentUser();
+        String assignee = isBlank(assignedUser) ? warning.getHandler() : assignedUser;
+        if (isBlank(assignee)) assignee = operator;
+        warning.setStatus("PROCESSING");
+        warning.setHandler(assignee);
+        warning.setPublishTime(warning.getPublishTime() == null ? now : warning.getPublishTime());
+        warning.setUpdateTime(now);
+        if (!updateById(warning)) return false;
+        addDisposalRecord(warningId, "CONFIRM", null, null, operator, confirmRemark);
+        createTodo(warningId, assignee, "预警处理", "TODO", "PROCESS");
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean misreportWarning(String warningId, String reason) {
+        EarlyWarning warning = getById(warningId);
+        if (warning == null) return false;
+        LocalDateTime now = LocalDateTime.now();
+        String operator = currentUser();
+        warning.setStatus("CLOSED");
+        warning.setRemark(reason);
+        warning.setUpdateTime(now);
+        if (!updateById(warning)) return false;
+        addDisposalRecord(warningId, "MISREPORT", null, null, operator, reason);
+        completeTodo(warningId);
+        generateArchive(warningId);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean submitWarningProcess(String warningId, String processContent, List<MultipartFile> files) {
+        EarlyWarning warning = getById(warningId);
+        if (warning == null || "CLOSED".equals(warning.getStatus())) return false;
+        LocalDateTime now = LocalDateTime.now();
+        String operator = currentUser();
+        warning.setStatus("HANDLED");
+        warning.setRemark(processContent);
+        warning.setUpdateTime(now);
+        if (!updateById(warning)) return false;
+        WarningDisposal disposal = new WarningDisposal();
+        disposal.setWarningId(warningId);
+        disposal.setDisposalType("HANDLE");
+        disposal.setDisposalUser(operator);
+        disposal.setDisposalContent(processContent);
+        disposal.setCreateTime(now);
+        warningDisposalMapper.insert(disposal);
+        if (files != null && !files.isEmpty()) {
+            saveAttachments(warningId, disposal.getDisposalId(), "PROCESS", files);
+        }
+        completeTodo(warningId);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean clearWarning(String warningId, String clearRemark) {
+        EarlyWarning warning = getById(warningId);
+        if (warning == null || "CLOSED".equals(warning.getStatus())) return false;
+        LocalDateTime now = LocalDateTime.now();
+        String operator = currentUser();
+        warning.setStatus("CLOSED");
+        warning.setRemark(clearRemark);
+        warning.setUpdateTime(now);
+        if (!updateById(warning)) return false;
+        addDisposalRecord(warningId, "CLEAR", null, null, operator, clearRemark);
+        completeTodo(warningId);
+        generateArchive(warningId);
+        return true;
+    }
+
+    private void generateArchive(String warningId) {
+        Map<String, Object> detail = earlyWarningMapper.selectWarningDetail(warningId);
+        if (detail != null) generateElectronicArchive(detail);
+    }
+
+    private String currentUser() {
+        try {
+            String username = SecurityUtils.getUsername();
+            return isBlank(username) ? "system" : username;
+        } catch (Exception ex) {
+            return "system";
+        }
+    }
+
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean upgradeWarning(String warningId, String newLevel, String disposalContent) {
@@ -336,6 +452,16 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean supervisionWarning(String warningId, String supervisionUser, String supervisionContent, String notificationType, String notificationContent, String attachmentUrl) {
+        WarningSupervision supervision = new WarningSupervision();
+        supervision.setWarningId(warningId);
+        supervision.setSupervisionUser(supervisionUser);
+        supervision.setSupervisionContent(supervisionContent);
+        supervision.setNotificationType(notificationType);
+        supervision.setNotificationContent(notificationContent);
+        supervision.setAttachmentUrl(attachmentUrl);
+        supervision.setCreateBy(currentUser());
+        supervision.setCreateTime(LocalDateTime.now());
+        warningSupervisionMapper.insert(supervision);
         WarningDisposal disposal = new WarningDisposal();
         disposal.setWarningId(warningId);
         disposal.setDisposalType("SUPERVISION");
@@ -517,7 +643,7 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         disposal.setDisposalType(disposalType);
         disposal.setOldLevel(oldLevel);
         disposal.setNewLevel(newLevel);
-        disposal.setDisposalUser(disposalUser);
+        disposal.setDisposalUser(isBlank(disposalUser) ? currentUser() : disposalUser);
         disposal.setDisposalContent(content);
         disposal.setCreateTime(LocalDateTime.now());
         warningDisposalMapper.insert(disposal);
@@ -539,12 +665,23 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
             map.put("attachmentType", attachment.getAttachmentType());
             map.put("attachmentSize", attachment.getAttachmentSize());
             map.put("createTime", attachment.getCreateTime());
+            String attachmentName = attachment.getAttachmentName();
+            map.put("attachmentStage", attachmentName != null && attachmentName.startsWith("[处理反馈]") ? "PROCESS" : "REPORT");
             result.add(map);
         }
         return result;
     }
 
     private void createTodo(String warningId, String userId, String taskName, String todoType) {
+        createTodo(warningId, userId, taskName, todoType, todoType);
+    }
+
+    @Override
+    public WarningAttachment getAttachment(String attachmentId) {
+        return warningAttachmentMapper.selectById(attachmentId);
+    }
+
+    private void createTodo(String warningId, String userId, String taskName, String todoType, String actionType) {
         WarningTodo todo = new WarningTodo();
         todo.setWarningId(warningId);
         todo.setUserId(userId);
@@ -556,6 +693,11 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         warningTodoMapper.insert(todo);
     }
 
+    @Override
+    public List<WarningSupervision> getSupervisionList(String warningId) {
+        return warningSupervisionMapper.selectByWarningId(warningId);
+    }
+
     private void completeTodo(String warningId) {
         LambdaQueryWrapper<WarningTodo> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(WarningTodo::getWarningId, warningId)
@@ -787,4 +929,54 @@ public class EarlyWarningServiceImpl extends ServiceImpl<EarlyWarningMapper, Ear
         }
         return contentType.substring(0, ATTACHMENT_TYPE_MAX_LEN);
     }
+
+    private static final Set<String> ALLOWED_ATTACHMENT_EXTENSIONS = new HashSet<>(Arrays.asList(
+            "jpg", "jpeg", "png", "gif", "webp", "pdf", "xls", "xlsx", "doc", "docx"
+    ));
+
+    private void validateAttachment(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("上传文件不能为空");
+        }
+        String originalName = file.getOriginalFilename();
+        String extension = "";
+        if (originalName != null && originalName.lastIndexOf('.') >= 0) {
+            extension = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
+        }
+        if (!ALLOWED_ATTACHMENT_EXTENSIONS.contains(extension)) {
+            throw new IllegalArgumentException("仅支持图片、PDF、Excel、Word文件");
+        }
+        String contentType = file.getContentType();
+        if (contentType != null && !contentType.trim().isEmpty()
+                && !"application/octet-stream".equalsIgnoreCase(contentType)
+                && !isCompatibleAttachmentType(extension, contentType)) {
+            throw new IllegalArgumentException("附件类型与文件扩展名不匹配");
+        }
+        if (file.getSize() > 20 * 1024 * 1024L) {
+            throw new IllegalArgumentException("单个附件不能超过20MB");
+        }
+    }
+
+    private boolean isCompatibleAttachmentType(String extension, String contentType) {
+        String type = contentType.toLowerCase(Locale.ROOT);
+        if ("jpg".equals(extension) || "jpeg".equals(extension) || "png".equals(extension)
+                || "gif".equals(extension) || "webp".equals(extension)) {
+            return type.startsWith("image/");
+        }
+        if ("pdf".equals(extension)) {
+            return "application/pdf".equals(type);
+        }
+        if ("xls".equals(extension)) {
+            return "application/vnd.ms-excel".equals(type) || "application/octet-stream".equals(type);
+        }
+        if ("xlsx".equals(extension)) {
+            return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet".equals(type)
+                    || "application/zip".equals(type) || "application/octet-stream".equals(type);
+        }
+        if ("doc".equals(extension)) {
+            return "application/msword".equals(type) || "application/octet-stream".equals(type);
+        }
+        return "application/vnd.openxmlformats-officedocument.wordprocessingml.document".equals(type)
+                || "application/zip".equals(type) || "application/octet-stream".equals(type);
+    }
 }

+ 21 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/AlarmMonitorListInDTO.java

@@ -0,0 +1,21 @@
+package com.zksy.manhole.dto.in;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 窨井盖实时监测-设备报警监控列表入参
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "窨井盖实时监测-设备报警监控列表入参", description = "窨井盖实时监测-设备报警监控列表入参")
+public class AlarmMonitorListInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "关键字,支持按设备编号、设备名称模糊查询")
+    private String keyword;
+}

+ 34 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/MaintenanceDataStatDashboardInDTO.java

@@ -0,0 +1,34 @@
+package com.zksy.manhole.dto.in;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 运维看板统计数据-入参
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "运维看板统计数据-入参", description = "运维看板统计数据-入参")
+public class MaintenanceDataStatDashboardInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "趋势类型:1-周度 2-月度,默认月度")
+    private Integer trendType;
+
+    @ApiModelProperty(value = "统计开始日期")
+    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date startDate;
+
+    @ApiModelProperty(value = "统计结束日期")
+    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date endDate;
+
+    @ApiModelProperty(value = "运维部门ID")
+    private Long deptId;
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceListInDTO.java

@@ -25,9 +25,18 @@ public class ManholeDeviceListInDTO implements Serializable {
     @ApiModelProperty(value = "在线状态(0-离线,1-在线)")
     private Integer onlineStatus;
 
+    @ApiModelProperty(value = "报警级别:0-有报警,1-严重报警,2-一般报警,不传则查询全部设备")
+    private Integer alarmLevel;
+
     @ApiModelProperty(value = "关键字,支持按设备编号、设备名称、设备位置模糊查询")
     private String keyword;
 
     @ApiModelProperty("是否查询井盖监测数据:默认不查询")
     private Boolean isQueryManholeData = false;
+
+    @ApiModelProperty(value = "当前记录起始索引,不传则不分页")
+    private Long pageNum;
+
+    @ApiModelProperty(value = "每页显示记录数,不传则不分页")
+    private Long pageSize;
 }

+ 24 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceStatisticsInDTO.java

@@ -0,0 +1,24 @@
+package com.zksy.manhole.dto.in;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 窨井设备统计(区域分布/类型分布)-入参
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "窨井设备统计-入参", description = "窨井设备统计-入参")
+public class ManholeDeviceStatisticsInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "所属区域(片区),为空统计全部区域")
+    private String district;
+
+    @ApiModelProperty(value = "设备类型ID(9=排水窨井设备,10=供水窨井设备),为空统计全部类型")
+    private String equipmentTypeId;
+}

+ 10 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderAddInDTO.java

@@ -24,9 +24,12 @@ public class WorkOrderAddInDTO implements Serializable {
     @ApiModelProperty(value = "案件ID")
     private Long caseId;
 
-    @ApiModelProperty(value = "设备ID", required = true)
+    @ApiModelProperty(value = "设备ID", required = false)
     private String deviceId;
 
+    @ApiModelProperty(value = "设备编码")
+    private String deviceCode;
+
     @ApiModelProperty(value = "工单类型:1-故障维修 2-日常巡检 3-设备保养", required = true)
     private Integer orderType;
 
@@ -42,4 +45,10 @@ public class WorkOrderAddInDTO implements Serializable {
     @ApiModelProperty(value = "计划完成时间")
     @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date planFinishTime;
+
+    @ApiModelProperty(value = "维修前图片 URL")
+    private String maintenanceBeforeImg;
+
+    @ApiModelProperty(value = "维修后图片 URL")
+    private String maintenanceAfterImg;
 }

+ 4 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderOperInDTO.java

@@ -39,4 +39,8 @@ public class WorkOrderOperInDTO implements Serializable {
     @ApiModelProperty(value = "延期截止时间(操作类型为 8-申请延期 时,必填)")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date delayedDeadline;
+
+    private String maintenanceBeforeImg;
+
+    private String maintenanceAfterImg;
 }

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderPageInDTO.java

@@ -22,6 +22,9 @@ public class WorkOrderPageInDTO implements Serializable {
     @ApiModelProperty(value = "案件编号")
     private String caseNo;
 
+    @ApiModelProperty(value = "设备ID")
+    private String deviceId;
+
     @ApiModelProperty(value = "当前记录起始索引", required = true)
     private Long pageNum;
 

+ 26 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/AlarmMonitorListOutDTO.java

@@ -0,0 +1,26 @@
+package com.zksy.manhole.dto.out;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 窨井盖实时监测-设备报警监控列表出参(设备列表 + 统计信息)
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "窨井盖实时监测-设备报警监控列表出参", description = "设备列表 + 统计信息")
+public class AlarmMonitorListOutDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "设备列表(含最新 jg_device_data 记录)")
+    private List<EquipmentBaseOutDTO> devices;
+
+    @ApiModelProperty(value = "统计信息:total/critical/warning/normal/onlineCount/avgBattery")
+    private Map<String, Object> statistics;
+}

+ 7 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/EquipmentBaseOutDTO.java

@@ -1,5 +1,6 @@
 package com.zksy.manhole.dto.out;
 
+import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.domain.EquipmentBase;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -29,4 +30,10 @@ public class EquipmentBaseOutDTO extends EquipmentBase implements Serializable {
 
     @ApiModelProperty(value = "设备预警阈值列表")
     private List<WarningThresholdOutDTO> warningThresholdList;
+
+    @ApiModelProperty(value = "最新报警数据 (来自 alarm_data 表)")
+    private AlarmData alarmData;
+
+    @ApiModelProperty(value = "是否有未办结工单")
+    private Boolean workOrderSubmitted;
 }

+ 40 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/MaintenanceDataStatDashboardOutDTO.java

@@ -0,0 +1,40 @@
+package com.zksy.manhole.dto.out;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 运维看板统计数据-出参
+ * 一次返回页面全部业务数据:KPI指标、4类图表、异常终端列表
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "运维看板统计数据-出参", description = "运维看板统计数据-出参")
+public class MaintenanceDataStatDashboardOutDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "KPI指标:totalTerminal/abnormalTerminal/abnormalRate/submittedCount/solvedRate/totalCase/disposalRate/delayRate/avgDisposalTime")
+    private Map<String, Object> kpi;
+
+    @ApiModelProperty(value = "异常终端数量趋势(trendLabel/abnormalTerminal/totalTerminal)")
+    private List<Map<String, Object>> abnormalTerminalTrend = new ArrayList<>();
+
+    @ApiModelProperty(value = "案件处置率/延期率趋势(trendLabel/caseDisposalRate/caseDelayRate)")
+    private List<Map<String, Object>> caseRateTrend = new ArrayList<>();
+
+    @ApiModelProperty(value = "问题类型分布(name/value)")
+    private List<Map<String, Object>> problemTypeDistribution = new ArrayList<>();
+
+    @ApiModelProperty(value = "区域运维排行(name/total/abnormal)")
+    private List<Map<String, Object>> regionRank = new ArrayList<>();
+
+    @ApiModelProperty(value = "异常终端设备列表(含manholeData/equipmentStatus)")
+    private List<EquipmentBaseOutDTO> abnormalDevices = new ArrayList<>();
+}

+ 24 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeDataOutDTO.java

@@ -1,10 +1,12 @@
 package com.zksy.manhole.dto.out;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.util.Date;
 
 /**
  * 井盖实时监测数据-出参
@@ -16,12 +18,27 @@ import java.io.Serializable;
 public class ManholeDataOutDTO implements Serializable {
     private static final long serialVersionUID = 1L;
 
+    @ApiModelProperty(value = "主键ID")
+    private String id;
+
     @ApiModelProperty(value = "电池电量")
     private String batteryLevel;
 
+    @ApiModelProperty(value = "上传时间")
+    private String uploadTime;
+
+    @ApiModelProperty(value = "温度值")
+    private String temperatureValue;
+
     @ApiModelProperty(value = "信号量")
     private String signalStrength;
 
+    @ApiModelProperty(value = "综合报警状态(0正常,1报警)")
+    private String alarmStatus;
+
+    @ApiModelProperty(value = "角度报警阈值")
+    private String angleAlarmThreshold;
+
     @ApiModelProperty(value = "倾斜角度")
     private String tiltAngle;
 
@@ -33,4 +50,11 @@ public class ManholeDataOutDTO implements Serializable {
 
     @ApiModelProperty(value = "水位报警状态(0正常,1报警)")
     private String waterLevelAlarmStatus;
+
+    @ApiModelProperty(value = "IMEI 卡号")
+    private String imeiCardNumber;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
 }

+ 37 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeLayerStatisticsOutDTO.java

@@ -0,0 +1,37 @@
+package com.zksy.manhole.dto.out;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 态势图层状态统计信息出参
+ */
+@Data
+@ApiModel(value = "态势图层状态统计", description = "态势图层状态统计")
+public class ManholeLayerStatisticsOutDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("设备总数")
+    private Integer total;
+
+    @ApiModelProperty("在用设备数量")
+    private Integer inUse;
+
+    @ApiModelProperty("维修设备数量")
+    private Integer maintenance;
+
+    @ApiModelProperty("报警设备数量")
+    private Integer alarm;
+
+    @ApiModelProperty("正常设备数量")
+    private Integer normal;
+
+    @ApiModelProperty("在线设备数量")
+    private Integer online;
+
+    @ApiModelProperty("离线/停用设备数量")
+    private Integer offline;
+}

+ 3 - 2
pipe-network-service/zksy-system/src/main/resources/mapper/WorkOrderMapper.xml

@@ -25,7 +25,8 @@
             <result property="planFinishTime" column="plan_finish_time" jdbcType="TIMESTAMP"/>
             <result property="realFinishTime" column="real_finish_time" jdbcType="TIMESTAMP"/>
             <result property="repairResult" column="repair_result" jdbcType="VARCHAR"/>
-            <result property="workImage" column="work_image" jdbcType="VARCHAR"/>
+            <result property="maintenanceBeforeImg" column="maintenance_before_img" jdbcType="VARCHAR"/>
+            <result property="maintenanceAfterImg" column="maintenance_after_img" jdbcType="VARCHAR"/>
             <result property="verifyUser" column="verify_user" jdbcType="BIGINT"/>
             <result property="verifyTime" column="verify_time" jdbcType="TIMESTAMP"/>
             <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
@@ -39,7 +40,7 @@
         order_desc,dept_id,dispatch_user,
         dispatch_time,receive_user,order_status,
         delayed_reason,delayed_deadline,plan_finish_time,
-        real_finish_time,repair_result,work_image,verify_user,
+        real_finish_time,repair_result,maintenance_before_img,maintenance_after_img,verify_user,
         verify_time,create_time,update_time
     </sql>
 </mapper>

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

@@ -9,6 +9,8 @@
         <result column="warning_type" property="warningType"/>
         <result column="warning_level" property="warningLevel"/>
         <result column="warning_special" property="warningSpecial"/>
+        <result column="device_code" property="deviceCode"/>
+        <result column="device_name" property="deviceName"/>
         <result column="location" property="location"/>
         <result column="longitude" property="longitude"/>
         <result column="latitude" property="latitude"/>
@@ -28,6 +30,7 @@
 
     <sql id="Base_Column_List">
         warning_id, warning_no, warning_name, warning_type, warning_level, warning_special,
+        device_code, device_name,
         location, longitude, latitude, ownership_unit, publisher, publish_time, handler,
         status, warning_content, process_instance_id, create_by, create_time, update_by,
         update_time, remark
@@ -439,9 +442,18 @@
             <if test="publisher != null and publisher != ''">
                 AND w.publisher LIKE CONCAT('%', #{publisher}, '%')
             </if>
+            <if test="excludePublisher != null and excludePublisher != ''">
+                AND (w.publisher IS NULL OR w.publisher NOT LIKE CONCAT('%', #{excludePublisher}, '%'))
+            </if>
             <if test="ownershipUnit != null and ownershipUnit != ''">
                 AND w.ownership_unit LIKE CONCAT('%', #{ownershipUnit}, '%')
             </if>
+            <if test="deviceCode != null and deviceCode != ''">
+                AND w.device_code LIKE CONCAT('%', #{deviceCode}, '%')
+            </if>
+            <if test="deviceName != null and deviceName != ''">
+                AND w.device_name LIKE CONCAT('%', #{deviceName}, '%')
+            </if>
             <if test="startTime != null">
                 AND w.create_time >= #{startTime}
             </if>

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

@@ -8,6 +8,10 @@
             CASE d.disposal_type
                 WHEN 'UPGRADE' THEN '预警升级'
                 WHEN 'DOWNGRADE' THEN '预警降级'
+                WHEN 'CONFIRM' THEN '确认异常'
+                WHEN 'MISREPORT' THEN '误报清除'
+                WHEN 'HANDLE' THEN '提交处理反馈'
+                WHEN 'CLEAR' THEN '清除预警'
                 WHEN 'RESOLVE' THEN '解除预警'
                 WHEN 'RETURN' THEN '退回重办'
                 WHEN 'SUPERVISION' THEN '预警督办'

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

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.zksy.base.warning.mapper.WarningSupervisionMapper">
+    <select id="selectByWarningId" resultType="com.zksy.base.warning.domain.WarningSupervision">
+        SELECT * FROM warning_supervision
+        WHERE warning_id = #{warningId}
+        ORDER BY create_time DESC
+    </select>
+</mapper>

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

@@ -43,6 +43,16 @@ public class EarlyWarning implements Serializable {
     @ApiModelProperty("预警专项")
     private String warningSpecial;
 
+
+    @TableField("device_code")
+    @ApiModelProperty("关联设备编码")
+    private String deviceCode;
+
+    @TableField("device_name")
+    @ApiModelProperty("关联设备名称")
+    private String deviceName;
+
+
     @TableField("location")
     @ApiModelProperty("预警位置")
     private String location;

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

@@ -150,10 +150,16 @@ public class WorkOrder implements Serializable {
     private String repairResult;
 
     /**
-     * 工单图片(多个图片用逗号分隔)
+     * 维修前图片 URL,多个图片使用逗号分隔
      */
-    @ApiModelProperty(value = "工单图片,多个用逗号分隔")
-    private String workImage;
+    @ApiModelProperty(value = "维修前图片 URL")
+    private String maintenanceBeforeImg;
+
+    /**
+     * 维修后图片 URL,多个图片使用逗号分隔
+     */
+    @ApiModelProperty(value = "维修后图片 URL")
+    private String maintenanceAfterImg;
 
     /**
      * 验收人ID
@@ -181,4 +187,4 @@ public class WorkOrder implements Serializable {
     @ApiModelProperty(value = "更新时间")
     @JsonFormat(locale = "zh",pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
     private Date updateTime;
-}
+}

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

@@ -67,7 +67,7 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
                     minValue, maxValue, actualValue, result, extraRemark);
 
             if (existing != null) {
-                updateExisting(existing, result, content, warningType);
+                updateExisting(existing, result, content, warningType, deviceCode, actualValue, minValue, maxValue);
                 return result.getLevel();
             }
 
@@ -111,15 +111,21 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
     }
 
     private void updateExisting(EarlyWarning existing, EarlyWarningLevelResult result,
-                                String content, String warningType) {
+                                String content, String warningType, String deviceCode,
+                                BigDecimal actualValue, BigDecimal minValue, BigDecimal maxValue) {
         String newLevel = String.valueOf(result.getLevel());
         boolean levelChanged = !newLevel.equals(existing.getWarningLevel());
         existing.setWarningLevel(newLevel);
         existing.setWarningContent(content);
+        existing.setDeviceCode(deviceCode);
         if (warningType != null && !warningType.isEmpty()) {
             existing.setWarningType(warningType);
             existing.setWarningName(buildWarningName(warningType, result.getLevel()));
         }
+        EquipmentBase equipment = findEquipment(deviceCode);
+        if (equipment != null && equipment.getEquipmentName() != null) {
+            existing.setDeviceName(equipment.getEquipmentName());
+        }
         existing.setUpdateBy(SYSTEM_USER);
         existing.setUpdateTime(LocalDateTime.now());
         earlyWarningApiMapper.updateById(existing);
@@ -142,6 +148,7 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
         warning.setWarningType(warningType != null && !warningType.isEmpty() ? warningType : warningCode);
         warning.setWarningLevel(String.valueOf(result.getLevel()));
         warning.setWarningSpecial(warningCode);
+        warning.setDeviceCode(deviceCode);
         warning.setStatus("RELEASED");
         warning.setPublisher(SYSTEM_USER);
         warning.setPublishTime(now);
@@ -152,6 +159,7 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
         warning.setRemark(buildRemarkMarker(deviceCode, warningCode));
 
         if (equipment != null) {
+            warning.setDeviceName(equipment.getEquipmentName());
             warning.setLocation(equipment.getEquipmentLocation());
             warning.setLongitude(equipment.getLongitude());
             warning.setLatitude(equipment.getLatitude());