Przeglądaj źródła

- 添加 供水管网-监测报警模块 实现监测报警管理、报警审核、报警处置全流程跟踪、监测报警一张图、报警阈值管理功能
- 添加 设备阈值分时管理功能

Kazerin 3 tygodni temu
rodzic
commit
5aa12c0efd
18 zmienionych plików z 1681 dodań i 2 usunięć
  1. 149 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmController.java
  2. 111 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyThresholdController.java
  3. 121 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/domain/AlarmAudit.java
  4. 44 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/AlarmAuditInDTO.java
  5. 48 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/AlarmDispatchInDTO.java
  6. 41 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/WaterThresholdSaveInDTO.java
  7. 53 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmAuditOutDTO.java
  8. 32 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmFlowOutDTO.java
  9. 54 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmMapPointOutDTO.java
  10. 35 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmStatsOutDTO.java
  11. 97 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/WaterAlarmOutDTO.java
  12. 13 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/mapper/AlarmAuditMapper.java
  13. 73 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/IWaterSupplyAlarmService.java
  14. 45 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/IWaterSupplyThresholdService.java
  15. 537 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java
  16. 198 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyThresholdServiceImpl.java
  17. 15 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/WarningThreshold.java
  18. 15 1
      zk-api-service/src/main/java/com/zksy/api/domain/WarningThreshold.java

+ 149 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyAlarmController.java

@@ -0,0 +1,149 @@
+package com.zksy.web.controller.WaterSupply.MonitorAlarm;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmAuditInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmDispatchInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmFlowOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyAlarmService;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 供水管网-监测报警接口
+ * 包含:报警管理(统计/实时/历史/详情/标记处理/派发工单)、报警审核、全流程跟踪、监测报警一张图
+ */
+@Slf4j
+@RestController
+@RequestMapping("/waterSupply/alarm")
+@Api(tags = "供水管网-监测报警")
+public class WaterSupplyAlarmController {
+
+    @Autowired
+    private IWaterSupplyAlarmService waterSupplyAlarmService;
+
+    @GetMapping("/stats")
+    @ApiOperation("监测报警统计(实时报警数/今日报警总数/设备异常率/平均响应时间)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
+    public AjaxResult getAlarmStats() {
+        AlarmStatsOutDTO stats = waterSupplyAlarmService.getAlarmStats();
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/realtime")
+    @ApiOperation("设备实时异常列表(未处理报警,信息卡片)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
+    public AjaxResult getRealtimeAlarmList() {
+        List<WaterAlarmOutDTO> list = waterSupplyAlarmService.getRealtimeAlarmList();
+        return AjaxResult.success(list);
+    }
+
+    @GetMapping("/history")
+    @ApiOperation("历史异常信息分页查询(支持按时间、设备、预警类型、等级、状态查询)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
+    public AjaxResult getHistoryAlarmPage(
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") long pageNum,
+            @ApiParam(value = "每页数量", defaultValue = "10") @RequestParam(defaultValue = "10") long pageSize,
+            @ApiParam(value = "设备编码") @RequestParam(required = false) String deviceCode,
+            @ApiParam(value = "预警类型") @RequestParam(required = false) String warningType,
+            @ApiParam(value = "报警等级 1-4") @RequestParam(required = false) Integer alarmLevel,
+            @ApiParam(value = "报警状态 0-未处理 1-已处理") @RequestParam(required = false) Integer alarmStatus,
+            @ApiParam(value = "开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false)
+            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+            @ApiParam(value = "结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false)
+            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @ApiParam(value = "关键字(设备编码/预警类型/预警编码)") @RequestParam(required = false) String keyword) {
+        Page<WaterAlarmOutDTO> page = waterSupplyAlarmService.getHistoryAlarmPage(
+                pageNum, pageSize, deviceCode, warningType, alarmLevel, alarmStatus, startTime, endTime, keyword);
+        return AjaxResult.success(page);
+    }
+
+    @GetMapping("/detail/{alarmId}")
+    @ApiOperation("异常详情(报警数据 + 设备数据 + 关联工单)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
+    public AjaxResult getAlarmDetail(@PathVariable String alarmId) {
+        WaterAlarmOutDTO detail = waterSupplyAlarmService.getAlarmDetail(alarmId);
+        return detail != null ? AjaxResult.success(detail) : AjaxResult.error("报警信息不存在");
+    }
+
+    @PutMapping("/resolve/{alarmId}")
+    @ApiOperation("一键标记已处理")
+    @Log(title = "供水报警标记已处理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:handle')")
+    public AjaxResult resolveAlarm(@PathVariable String alarmId,
+                                   @ApiParam(value = "处理备注") @RequestParam(required = false) String handleRemark) {
+        try {
+            boolean result = waterSupplyAlarmService.resolveAlarm(alarmId, handleRemark);
+            return result ? AjaxResult.success("处理成功") : AjaxResult.error("处理失败");
+        } catch (Exception e) {
+            log.error("标记报警已处理失败:alarmId={}", alarmId, e);
+            return AjaxResult.error("处理失败:" + e.getMessage());
+        }
+    }
+
+    @PostMapping("/dispatch")
+    @ApiOperation("将报警派发为运维工单")
+    @Log(title = "供水报警派发工单", businessType = BusinessType.INSERT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:dispatch')")
+    public AjaxResult dispatchToWorkOrder(@RequestBody AlarmDispatchInDTO inDTO) {
+        try {
+            boolean result = waterSupplyAlarmService.dispatchToWorkOrder(inDTO);
+            return result ? AjaxResult.success("工单派发成功") : AjaxResult.error("工单派发失败");
+        } catch (Exception e) {
+            log.error("供水报警派发工单失败:alarmId={}", inDTO != null ? inDTO.getAlarmId() : null, e);
+            return AjaxResult.error("工单派发失败:" + e.getMessage());
+        }
+    }
+
+    @PostMapping("/audit")
+    @ApiOperation("报警审核/解除(支持意见录入与附件上传)")
+    @Log(title = "供水报警审核", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:audit')")
+    public AjaxResult auditAlarm(@RequestBody AlarmAuditInDTO inDTO) {
+        try {
+            AlarmAuditOutDTO audit = waterSupplyAlarmService.auditAlarm(inDTO);
+            return AjaxResult.success("审核操作成功", audit);
+        } catch (Exception e) {
+            log.error("供水报警审核失败:alarmId={}", inDTO != null ? inDTO.getAlarmId() : null, e);
+            return AjaxResult.error("审核操作失败:" + e.getMessage());
+        }
+    }
+
+    @GetMapping("/audit/list")
+    @ApiOperation("查询报警审核记录列表")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
+    public AjaxResult getAuditList(@ApiParam(value = "报警ID", required = true) @RequestParam String alarmId) {
+        return AjaxResult.success(waterSupplyAlarmService.getAuditList(alarmId));
+    }
+
+    @GetMapping("/flow/{alarmId}")
+    @ApiOperation("报警处置全流程跟踪(报警+审核记录+工单+工单操作日志)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
+    public AjaxResult getAlarmFlow(@PathVariable String alarmId) {
+        AlarmFlowOutDTO flow = waterSupplyAlarmService.getAlarmFlow(alarmId);
+        return flow != null && flow.getAlarm() != null ? AjaxResult.success(flow) : AjaxResult.error("报警信息不存在");
+    }
+
+    @GetMapping("/map/points")
+    @ApiOperation("监测报警一张图-报警点位(按报警级别着色)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:list')")
+    public AjaxResult getAlarmMapPoints() {
+        List<AlarmMapPointOutDTO> points = waterSupplyAlarmService.getAlarmMapPoints();
+        return AjaxResult.success(points);
+    }
+}

+ 111 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/MonitorAlarm/WaterSupplyThresholdController.java

@@ -0,0 +1,111 @@
+package com.zksy.web.controller.WaterSupply.MonitorAlarm;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.WaterThresholdSaveInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyThresholdService;
+import com.zksy.base.alarm.domain.WarningThreshold;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 供水管网-监测报警阈值管理接口
+ * 支持批量/分类设置传感器阈值、分时阈值、分类查询阈值详情
+ */
+@Slf4j
+@RestController
+@RequestMapping("/waterSupply/alarm/threshold")
+@Api(tags = "供水管网-监测报警阈值管理")
+public class WaterSupplyThresholdController {
+
+    @Autowired
+    private IWaterSupplyThresholdService waterSupplyThresholdService;
+
+    @GetMapping("/page")
+    @ApiOperation("阈值分页查询(分类查询阈值详情)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:threshold:list')")
+    public AjaxResult getThresholdPage(
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") long pageNum,
+            @ApiParam(value = "每页数量", defaultValue = "10") @RequestParam(defaultValue = "10") long pageSize,
+            @ApiParam(value = "设备编码") @RequestParam(required = false) String deviceCode,
+            @ApiParam(value = "预警类型") @RequestParam(required = false) String warningType,
+            @ApiParam(value = "预警编码") @RequestParam(required = false) String warningCode,
+            @ApiParam(value = "关键字") @RequestParam(required = false) String keyword) {
+        Page<WarningThreshold> page = waterSupplyThresholdService.findThresholdPage(
+                pageNum, pageSize, deviceCode, warningType, warningCode, keyword);
+        return AjaxResult.success(page);
+    }
+
+    @GetMapping("/list")
+    @ApiOperation("阈值列表查询(分类查询阈值详情)")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:threshold:list')")
+    public AjaxResult getThresholdList(
+            @ApiParam(value = "设备编码") @RequestParam(required = false) String deviceCode,
+            @ApiParam(value = "预警类型") @RequestParam(required = false) String warningType,
+            @ApiParam(value = "预警编码") @RequestParam(required = false) String warningCode) {
+        return AjaxResult.success(waterSupplyThresholdService.findThresholdList(deviceCode, warningType, warningCode));
+    }
+
+    @GetMapping("/detail/{id}")
+    @ApiOperation("阈值详情")
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:threshold:list')")
+    public AjaxResult getThresholdDetail(@PathVariable String id) {
+        try {
+            return AjaxResult.success(waterSupplyThresholdService.getThresholdDetail(id));
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    @PostMapping("/save")
+    @ApiOperation("批量保存阈值(一个规则应用到多个设备,支持分时阈值)")
+    @Log(title = "供水报警阈值批量保存", businessType = BusinessType.INSERT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:threshold:add')")
+    public AjaxResult saveThresholdBatch(@RequestBody WaterThresholdSaveInDTO inDTO) {
+        try {
+            int count = waterSupplyThresholdService.saveThresholdBatch(inDTO);
+            return AjaxResult.success("保存成功,共设置" + count + "条阈值", count);
+        } catch (Exception e) {
+            log.error("供水报警阈值批量保存失败", e);
+            return AjaxResult.error("保存失败:" + e.getMessage());
+        }
+    }
+
+    @PutMapping("/update")
+    @ApiOperation("修改阈值")
+    @Log(title = "供水报警阈值修改", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:threshold:edit')")
+    public AjaxResult updateThreshold(@RequestBody WarningThreshold threshold) {
+        try {
+            boolean result = waterSupplyThresholdService.updateThreshold(threshold);
+            return result ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
+        } catch (Exception e) {
+            log.error("供水报警阈值修改失败", e);
+            return AjaxResult.error("修改失败:" + e.getMessage());
+        }
+    }
+
+    @DeleteMapping("/delete")
+    @ApiOperation("批量删除阈值")
+    @Log(title = "供水报警阈值删除", businessType = BusinessType.DELETE)
+    @PreAuthorize("@ss.hasPermi('waterSupply:alarm:threshold:remove')")
+    public AjaxResult deleteThresholdBatch(@ApiParam(value = "阈值ID列表", required = true)
+                                           @RequestParam("ids") List<String> ids) {
+        try {
+            boolean result = waterSupplyThresholdService.deleteThresholdBatch(ids);
+            return result ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+        } catch (Exception e) {
+            log.error("供水报警阈值删除失败", e);
+            return AjaxResult.error("删除失败:" + e.getMessage());
+        }
+    }
+}

+ 121 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/domain/AlarmAudit.java

@@ -0,0 +1,121 @@
+package com.zksy.WaterSupply.MonitorAlarm.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.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 供水监测报警审核记录实体
+ */
+@Data
+@TableName(value = "alarm_audit", schema = "app_user")
+@ApiModel(
+        value = "AlarmAudit",
+        description = "报警审核记录,包含报警审核/解除操作、意见、附件及全流程跟踪所需字段"
+)
+public class AlarmAudit implements Serializable {
+
+    @ApiModelProperty(
+            value = "审核记录ID",
+            example = "1",
+            position = 1,
+            notes = "数据库自增主键,新增时不需要传入"
+    )
+    @TableId(type = IdType.AUTO)
+    private Long auditId;
+
+    @ApiModelProperty(
+            value = "关联报警ID",
+            example = "uuid",
+            required = true,
+            position = 2
+    )
+    private String alarmId;
+
+    @ApiModelProperty(
+            value = "审核动作",
+            example = "1",
+            required = true,
+            allowableValues = "1,2",
+            position = 3,
+            notes = "1-审核 2-解除"
+    )
+    private Integer auditAction;
+
+    @ApiModelProperty(
+            value = "审核结果",
+            example = "1",
+            required = true,
+            allowableValues = "1,2",
+            position = 4,
+            notes = "1-属实/审核通过 2-误报/解除报警"
+    )
+    private Integer auditResult;
+
+    @ApiModelProperty(
+            value = "审核/解除意见",
+            example = "现场核实确为管网压力异常,同意派发工单处理",
+            position = 5
+    )
+    private String auditOpinion;
+
+    @ApiModelProperty(
+            value = "附件地址(多个用逗号分隔)",
+            example = "https://xx/upload/a.jpg,https://xx/upload/b.jpg",
+            position = 6
+    )
+    private String attachUrl;
+
+    @ApiModelProperty(
+            value = "操作人ID",
+            example = "1",
+            hidden = true,
+            position = 7
+    )
+    private Long auditUserId;
+
+    @ApiModelProperty(
+            value = "操作人姓名",
+            example = "admin",
+            hidden = true,
+            position = 8
+    )
+    private String auditUserName;
+
+    @ApiModelProperty(
+            value = "操作时间",
+            example = "2026-08-11 10:00:00",
+            position = 9
+    )
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime auditTime;
+
+    @ApiModelProperty(
+            value = "创建时间",
+            example = "2026-08-11 10:00:00",
+            hidden = true,
+            position = 10
+    )
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty(
+            value = "更新时间",
+            example = "2026-08-11 10:00:00",
+            hidden = true,
+            position = 11
+    )
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 44 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/AlarmAuditInDTO.java

@@ -0,0 +1,44 @@
+package com.zksy.WaterSupply.MonitorAlarm.dto.in;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 报警审核-入参
+ */
+@Data
+@ApiModel(value = "报警审核-入参", description = "报警审核/解除操作入参")
+public class AlarmAuditInDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "报警ID", required = true, example = "uuid")
+    private String alarmId;
+
+    @ApiModelProperty(
+            value = "审核动作",
+            required = true,
+            allowableValues = "1,2",
+            example = "1",
+            notes = "1-审核 2-解除"
+    )
+    private Integer auditAction;
+
+    @ApiModelProperty(
+            value = "审核结果",
+            required = true,
+            allowableValues = "1,2",
+            example = "1",
+            notes = "1-属实/审核通过 2-误报/解除报警"
+    )
+    private Integer auditResult;
+
+    @ApiModelProperty(value = "审核/解除意见", example = "现场核实确为管网压力异常,同意派发工单处理")
+    private String auditOpinion;
+
+    @ApiModelProperty(value = "附件地址(多个用逗号分隔)", example = "https://xx/upload/a.jpg")
+    private String attachUrl;
+}

+ 48 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/AlarmDispatchInDTO.java

@@ -0,0 +1,48 @@
+package com.zksy.WaterSupply.MonitorAlarm.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;
+
+/**
+ * 报警派发工单-入参
+ */
+@Data
+@ApiModel(value = "报警派发工单-入参", description = "将供水监测报警派发为运维工单的入参")
+public class AlarmDispatchInDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "报警ID", required = true, example = "uuid")
+    private String alarmId;
+
+    @ApiModelProperty(
+            value = "工单类型",
+            example = "1",
+            allowableValues = "1,2,3",
+            notes = "1-故障维修(默认) 2-日常巡检 3-设备保养"
+    )
+    private Integer orderType;
+
+    @ApiModelProperty(
+            value = "工单优先级",
+            example = "1",
+            allowableValues = "1,2,3",
+            notes = "1-紧急 2-一般 3-低;不传时按报警等级映射(1级→紧急,2/3级→一般,4级→低)"
+    )
+    private Integer orderLevel;
+
+    @ApiModelProperty(value = "工单问题描述,不传时取报警信息", example = "供水管网压力异常报警,请现场核实处理")
+    private String orderDesc;
+
+    @ApiModelProperty(value = "负责部门ID")
+    private Long deptId;
+
+    @ApiModelProperty(value = "计划完成时间")
+    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date planFinishTime;
+}

+ 41 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/in/WaterThresholdSaveInDTO.java

@@ -0,0 +1,41 @@
+package com.zksy.WaterSupply.MonitorAlarm.dto.in;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 供水监测报警阈值-保存入参(支持批量、分类设置与分时阈值)
+ */
+@Data
+@ApiModel(value = "供水监测报警阈值-保存入参", description = "支持一个阈值规则批量应用到多个设备,支持分时生效")
+public class WaterThresholdSaveInDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "设备编码列表(批量设置时传入多个)", required = true, example = "[\"SB001\",\"SB002\"]")
+    private java.util.List<String> deviceCodes;
+
+    @ApiModelProperty(value = "预警类型", required = true, example = "水位预警")
+    private String warningType;
+
+    @ApiModelProperty(value = "预警编码", required = true, example = "WARN-WATER-LEVEL")
+    private String warningCode;
+
+    @ApiModelProperty(value = "预警最小值,小于等于该值视为报警", example = "0.5")
+    private Double minValue;
+
+    @ApiModelProperty(value = "预警最大值,大于等于该值视为报警", example = "3.0")
+    private Double maxValue;
+
+    @ApiModelProperty(value = "分时生效开始时间(HH:mm),为空表示全天生效", example = "08:00")
+    private String periodStart;
+
+    @ApiModelProperty(value = "分时生效结束时间(HH:mm),为空表示全天生效", example = "20:00")
+    private String periodEnd;
+
+    @ApiModelProperty(value = "备注", example = "供水高峰期水位阈值")
+    private String remark;
+}

+ 53 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmAuditOutDTO.java

@@ -0,0 +1,53 @@
+package com.zksy.WaterSupply.MonitorAlarm.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.time.LocalDateTime;
+
+/**
+ * 报警审核记录-出参
+ */
+@Data
+@ApiModel(value = "报警审核记录-出参", description = "报警审核/解除操作记录")
+public class AlarmAuditOutDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "审核记录ID")
+    private Long auditId;
+
+    @ApiModelProperty(value = "报警ID")
+    private String alarmId;
+
+    @ApiModelProperty(value = "审核动作:1-审核 2-解除")
+    private Integer auditAction;
+
+    @ApiModelProperty(value = "审核动作文本", example = "审核")
+    private String auditActionText;
+
+    @ApiModelProperty(value = "审核结果:1-属实/审核通过 2-误报/解除报警")
+    private Integer auditResult;
+
+    @ApiModelProperty(value = "审核结果文本", example = "属实")
+    private String auditResultText;
+
+    @ApiModelProperty(value = "审核/解除意见")
+    private String auditOpinion;
+
+    @ApiModelProperty(value = "附件地址(多个用逗号分隔)")
+    private String attachUrl;
+
+    @ApiModelProperty(value = "操作人ID")
+    private Long auditUserId;
+
+    @ApiModelProperty(value = "操作人姓名")
+    private String auditUserName;
+
+    @ApiModelProperty(value = "操作时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime auditTime;
+}

+ 32 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmFlowOutDTO.java

@@ -0,0 +1,32 @@
+package com.zksy.WaterSupply.MonitorAlarm.dto.out;
+
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 报警处置全流程跟踪-出参
+ */
+@Data
+@ApiModel(value = "报警处置全流程跟踪-出参", description = "报警信息 + 审核记录 + 关联工单及操作日志")
+public class AlarmFlowOutDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "报警信息(含设备信息)")
+    private WaterAlarmOutDTO alarm;
+
+    @ApiModelProperty(value = "审核/解除记录列表")
+    private List<AlarmAuditOutDTO> auditList;
+
+    @ApiModelProperty(value = "关联工单(可能为空)")
+    private WorkOrder workOrder;
+
+    @ApiModelProperty(value = "工单操作日志列表")
+    private List<WorkOrderLog> workOrderLogList;
+}

+ 54 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmMapPointOutDTO.java

@@ -0,0 +1,54 @@
+package com.zksy.WaterSupply.MonitorAlarm.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.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 监测报警一张图-点位出参
+ */
+@Data
+@ApiModel(value = "监测报警一张图-点位出参", description = "报警点位信息,供 GIS 地图按报警级别着色展示")
+public class AlarmMapPointOutDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "报警ID")
+    private String alarmId;
+
+    @ApiModelProperty(value = "设备编码")
+    private String deviceCode;
+
+    @ApiModelProperty(value = "设备名称")
+    private String deviceName;
+
+    @ApiModelProperty(value = "设备位置")
+    private String equipmentLocation;
+
+    @ApiModelProperty(value = "经度")
+    private String longitude;
+
+    @ApiModelProperty(value = "纬度")
+    private String latitude;
+
+    @ApiModelProperty(value = "预警类型")
+    private String warningType;
+
+    @ApiModelProperty(value = "报警参数值")
+    private BigDecimal actualValue;
+
+    @ApiModelProperty(value = "报警等级:1-4,1级最高(用于地图着色)")
+    private Integer alarmLevel;
+
+    @ApiModelProperty(value = "报警等级文本")
+    private String alarmLevelText;
+
+    @ApiModelProperty(value = "报警时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime alarmTime;
+}

+ 35 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmStatsOutDTO.java

@@ -0,0 +1,35 @@
+package com.zksy.WaterSupply.MonitorAlarm.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 AlarmStatsOutDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "供水设备实时报警数(未处理报警去重设备数)", example = "3")
+    private long realtimeAlarmCount;
+
+    @ApiModelProperty(value = "今日报警总数", example = "12")
+    private long todayAlarmCount;
+
+    @ApiModelProperty(value = "供水设备总数", example = "120")
+    private long totalDeviceCount;
+
+    @ApiModelProperty(value = "异常设备数(存在未处理报警的设备数)", example = "3")
+    private long abnormalDeviceCount;
+
+    @ApiModelProperty(value = "设备异常率(百分比,保留两位小数)", example = "2.50")
+    private String deviceAbnormalRate;
+
+    @ApiModelProperty(value = "平均响应时间(分钟,今日已处理报警 处理时间-报警时间 均值)", example = "35.20")
+    private String avgResponseMinutes;
+}

+ 97 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/WaterAlarmOutDTO.java

@@ -0,0 +1,97 @@
+package com.zksy.WaterSupply.MonitorAlarm.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.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 供水监测报警信息-出参(实时异常卡片/历史列表/详情共用)
+ */
+@Data
+@ApiModel(value = "供水监测报警信息-出参", description = "报警基础信息 + 设备信息,字段参考报警数据表及设备数据表")
+public class WaterAlarmOutDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "报警ID", example = "uuid")
+    private String id;
+
+    @ApiModelProperty(value = "设备编码", example = "SB001")
+    private String deviceCode;
+
+    @ApiModelProperty(value = "设备名称", example = "XX供水压力监测点")
+    private String deviceName;
+
+    @ApiModelProperty(value = "设备ID", example = "EQ001")
+    private String equipmentId;
+
+    @ApiModelProperty(value = "设备类型(预警类型分类)", example = "压力监测")
+    private String deviceType;
+
+    @ApiModelProperty(value = "设备位置", example = "XX大道XX号")
+    private String equipmentLocation;
+
+    @ApiModelProperty(value = "经度", example = "113.12345678")
+    private String longitude;
+
+    @ApiModelProperty(value = "纬度", example = "23.12345678")
+    private String latitude;
+
+    @ApiModelProperty(value = "预警类型", example = "压力预警")
+    private String warningType;
+
+    @ApiModelProperty(value = "预警编码", example = "WARN-PRESSURE")
+    private String warningCode;
+
+    @ApiModelProperty(value = "报警参数值(实际值)", example = "0.32")
+    private BigDecimal actualValue;
+
+    @ApiModelProperty(value = "阈值下限", example = "0.40")
+    private BigDecimal minValue;
+
+    @ApiModelProperty(value = "阈值上限", example = "0.80")
+    private BigDecimal maxValue;
+
+    @ApiModelProperty(value = "偏差率(百分比)", example = "20.00")
+    private BigDecimal deviationRatio;
+
+    @ApiModelProperty(value = "报警等级:1-4,1级最高", example = "1")
+    private Integer alarmLevel;
+
+    @ApiModelProperty(value = "报警等级文本", example = "一级报警")
+    private String alarmLevelText;
+
+    @ApiModelProperty(value = "报警状态:0-未处理 1-已处理", example = "0")
+    private Integer alarmStatus;
+
+    @ApiModelProperty(value = "报警状态文本", example = "待处理")
+    private String alarmStatusText;
+
+    @ApiModelProperty(value = "报警时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime alarmTime;
+
+    @ApiModelProperty(value = "处理时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime handleTime;
+
+    @ApiModelProperty(value = "处理人")
+    private String handleUser;
+
+    @ApiModelProperty(value = "处理备注")
+    private String handleRemark;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "关联工单编号(已派发工单时返回)")
+    private String orderNo;
+
+    @ApiModelProperty(value = "关联工单状态(已派发工单时返回)")
+    private Integer orderStatus;
+}

+ 13 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/mapper/AlarmAuditMapper.java

@@ -0,0 +1,13 @@
+package com.zksy.WaterSupply.MonitorAlarm.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 报警审核记录 Mapper
+ */
+@Mapper
+public interface AlarmAuditMapper extends BaseMapper<AlarmAudit> {
+
+}

+ 73 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/IWaterSupplyAlarmService.java

@@ -0,0 +1,73 @@
+package com.zksy.WaterSupply.MonitorAlarm.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmAuditInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmDispatchInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmFlowOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 供水监测报警业务接口
+ */
+public interface IWaterSupplyAlarmService {
+
+    /**
+     * 监测报警统计(实时报警数、今日报警总数、设备异常率、平均响应时间)
+     */
+    AlarmStatsOutDTO getAlarmStats();
+
+    /**
+     * 实时异常报警列表(未处理报警,信息卡片展示)
+     */
+    List<WaterAlarmOutDTO> getRealtimeAlarmList();
+
+    /**
+     * 历史异常报警分页查询
+     */
+    Page<WaterAlarmOutDTO> getHistoryAlarmPage(long pageNum, long pageSize,
+                                               String deviceCode, String warningType,
+                                               Integer alarmLevel, Integer alarmStatus,
+                                               LocalDateTime startTime, LocalDateTime endTime,
+                                               String keyword);
+
+    /**
+     * 报警详情(报警数据 + 设备数据 + 关联工单)
+     */
+    WaterAlarmOutDTO getAlarmDetail(String alarmId);
+
+    /**
+     * 一键标记已处理
+     */
+    boolean resolveAlarm(String alarmId, String handleRemark);
+
+    /**
+     * 将报警派发为运维工单
+     */
+    boolean dispatchToWorkOrder(AlarmDispatchInDTO inDTO);
+
+    /**
+     * 报警审核/解除(意见 + 附件)
+     */
+    AlarmAuditOutDTO auditAlarm(AlarmAuditInDTO inDTO);
+
+    /**
+     * 查询报警审核记录列表
+     */
+    List<AlarmAuditOutDTO> getAuditList(String alarmId);
+
+    /**
+     * 报警处置全流程跟踪(报警 + 审核记录 + 工单 + 工单日志)
+     */
+    AlarmFlowOutDTO getAlarmFlow(String alarmId);
+
+    /**
+     * 监测报警一张图点位(未处理报警,含经纬度与报警级别)
+     */
+    List<AlarmMapPointOutDTO> getAlarmMapPoints();
+}

+ 45 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/IWaterSupplyThresholdService.java

@@ -0,0 +1,45 @@
+package com.zksy.WaterSupply.MonitorAlarm.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.WaterThresholdSaveInDTO;
+import com.zksy.base.alarm.domain.WarningThreshold;
+
+import java.util.List;
+
+/**
+ * 供水监测报警阈值业务接口
+ */
+public interface IWaterSupplyThresholdService {
+
+    /**
+     * 分页查询阈值(支持设备/分类/关键字)
+     */
+    Page<WarningThreshold> findThresholdPage(long pageNum, long pageSize,
+                                             String deviceCode, String warningType,
+                                             String warningCode, String keyword);
+
+    /**
+     * 查询阈值列表(分类查询阈值详情)
+     */
+    List<WarningThreshold> findThresholdList(String deviceCode, String warningType, String warningCode);
+
+    /**
+     * 根据ID查询阈值详情
+     */
+    WarningThreshold getThresholdDetail(String id);
+
+    /**
+     * 批量保存阈值(一个规则应用到多个设备,支持分时阈值)
+     */
+    int saveThresholdBatch(WaterThresholdSaveInDTO inDTO);
+
+    /**
+     * 修改阈值
+     */
+    boolean updateThreshold(WarningThreshold threshold);
+
+    /**
+     * 批量删除阈值
+     */
+    boolean deleteThresholdBatch(List<String> ids);
+}

+ 537 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java

@@ -0,0 +1,537 @@
+package com.zksy.WaterSupply.MonitorAlarm.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.MonitorAlarm.domain.AlarmAudit;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmAuditInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.AlarmDispatchInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmAuditOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmFlowOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmMapPointOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.AlarmStatsOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.dto.out.WaterAlarmOutDTO;
+import com.zksy.WaterSupply.MonitorAlarm.mapper.AlarmAuditMapper;
+import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyAlarmService;
+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.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.WorkOrderLogMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.common.core.domain.model.LoginUser;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 供水监测报警业务实现
+ */
+@Slf4j
+@Service
+public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
+
+    /**
+     * 供水设备顶级类型名称(equipment_type 中 parent_type_id='0' 的顶级类型)
+     */
+    @Value("${waterSupply.alarm.top-level-type:供水}")
+    private String waterTopLevelTypeName;
+
+    private static final DateTimeFormatter ORDER_NO_FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+    @Autowired
+    private AlarmDataService alarmDataService;
+
+    @Autowired
+    private EquipmentBaseService equipmentBaseService;
+
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
+    @Autowired
+    private WorkOrderLogMapper workOrderLogMapper;
+
+    @Autowired
+    private AlarmAuditMapper alarmAuditMapper;
+
+    // ------------------------------------------------------------------
+    // 供水设备范围解析
+    // ------------------------------------------------------------------
+
+    /**
+     * 解析全部供水设备编码(按设备类别顶级类型"供水"动态查询)
+     */
+    private Set<String> resolveWaterDeviceCodes() {
+        return equipmentBaseService.findByTopLevelType(waterTopLevelTypeName).stream()
+                .map(EquipmentBase::getEquipmentCode)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * 设备编码 -> 设备信息映射
+     */
+    private Map<String, EquipmentBase> loadEquipmentCodeMap() {
+        return equipmentBaseMapper.selectList(null).stream()
+                .collect(Collectors.toMap(EquipmentBase::getEquipmentCode, Function.identity(), (a, b) -> a));
+    }
+
+    /**
+     * 设备编码 -> 工单映射(同一报警派发多个工单时取最新)
+     */
+    private Map<String, WorkOrder> loadWorkOrderMapByAlarmIds(List<String> alarmIds) {
+        if (CollUtil.isEmpty(alarmIds)) {
+            return Collections.emptyMap();
+        }
+        List<WorkOrder> orders = workOrderMapper.selectList(new LambdaQueryWrapper<WorkOrder>()
+                .in(WorkOrder::getAlarmId, alarmIds)
+                .orderByDesc(WorkOrder::getOrderId));
+        return orders.stream()
+                .collect(Collectors.toMap(WorkOrder::getAlarmId, Function.identity(), (a, b) -> a));
+    }
+
+    // ------------------------------------------------------------------
+    // 统计
+    // ------------------------------------------------------------------
+
+    @Override
+    public AlarmStatsOutDTO getAlarmStats() {
+        AlarmStatsOutDTO stats = new AlarmStatsOutDTO();
+        Set<String> waterCodes = resolveWaterDeviceCodes();
+        stats.setTotalDeviceCount(waterCodes.size());
+        if (waterCodes.isEmpty()) {
+            stats.setDeviceAbnormalRate("0.00");
+            stats.setAvgResponseMinutes("0.00");
+            return stats;
+        }
+
+        // 未处理报警(实时报警)
+        List<AlarmData> pendingAlarms = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
+                .in(AlarmData::getDeviceCode, waterCodes)
+                .eq(AlarmData::getAlarmStatus, 0));
+        Set<String> abnormalDevices = pendingAlarms.stream()
+                .map(AlarmData::getDeviceCode)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        stats.setRealtimeAlarmCount(abnormalDevices.size());
+        stats.setAbnormalDeviceCount(abnormalDevices.size());
+        stats.setDeviceAbnormalRate(waterCodes.isEmpty() ? "0.00"
+                : String.format("%.2f", abnormalDevices.size() * 100.0d / waterCodes.size()));
+
+        // 今日报警总数
+        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
+        long todayCount = alarmDataService.count(new LambdaQueryWrapper<AlarmData>()
+                .in(AlarmData::getDeviceCode, waterCodes)
+                .ge(AlarmData::getAlarmTime, todayStart));
+        stats.setTodayAlarmCount(todayCount);
+
+        // 平均响应时间:今日已处理报警(处理时间-报警时间)均值,单位分钟
+        List<AlarmData> handledToday = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
+                .in(AlarmData::getDeviceCode, waterCodes)
+                .eq(AlarmData::getAlarmStatus, 1)
+                .ge(AlarmData::getHandleTime, todayStart));
+        double totalSeconds = 0;
+        int count = 0;
+        for (AlarmData alarm : handledToday) {
+            if (alarm.getAlarmTime() == null || alarm.getHandleTime() == null) {
+                continue;
+            }
+            totalSeconds += alarm.getHandleTime().toEpochSecond(java.time.ZoneOffset.ofHours(8))
+                    - alarm.getAlarmTime().toEpochSecond(java.time.ZoneOffset.ofHours(8));
+            count++;
+        }
+        stats.setAvgResponseMinutes(count == 0 ? "0.00" : String.format("%.2f", totalSeconds / 60.0d / count));
+        return stats;
+    }
+
+    // ------------------------------------------------------------------
+    // 实时 / 历史 / 详情
+    // ------------------------------------------------------------------
+
+    @Override
+    public List<WaterAlarmOutDTO> getRealtimeAlarmList() {
+        Set<String> waterCodes = resolveWaterDeviceCodes();
+        if (waterCodes.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<AlarmData> alarms = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
+                .in(AlarmData::getDeviceCode, waterCodes)
+                .eq(AlarmData::getAlarmStatus, 0)
+                .orderByDesc(AlarmData::getAlarmTime));
+        return convertToVoList(alarms);
+    }
+
+    @Override
+    public Page<WaterAlarmOutDTO> getHistoryAlarmPage(long pageNum, long pageSize,
+                                                      String deviceCode, String warningType,
+                                                      Integer alarmLevel, Integer alarmStatus,
+                                                      LocalDateTime startTime, LocalDateTime endTime,
+                                                      String keyword) {
+        Page<AlarmData> page = new Page<>(pageNum, pageSize);
+        Set<String> waterCodes = resolveWaterDeviceCodes();
+        LambdaQueryWrapper<AlarmData> wrapper = new LambdaQueryWrapper<>();
+        if (!waterCodes.isEmpty()) {
+            wrapper.in(AlarmData::getDeviceCode, waterCodes);
+        } else {
+            wrapper.eq(AlarmData::getId, "-1");
+        }
+        wrapper.eq(StrUtil.isNotBlank(deviceCode), AlarmData::getDeviceCode, deviceCode)
+                .like(StrUtil.isNotBlank(warningType), AlarmData::getWarningType, warningType)
+                .eq(alarmLevel != null, AlarmData::getAlarmLevel, alarmLevel)
+                .eq(alarmStatus != null, AlarmData::getAlarmStatus, alarmStatus)
+                .ge(startTime != null, AlarmData::getAlarmTime, startTime)
+                .le(endTime != null, AlarmData::getAlarmTime, endTime)
+                .and(StrUtil.isNotBlank(keyword), w -> w.like(AlarmData::getDeviceCode, keyword)
+                        .or().like(AlarmData::getWarningType, keyword)
+                        .or().like(AlarmData::getWarningCode, keyword))
+                .orderByDesc(AlarmData::getAlarmTime);
+        Page<AlarmData> result = alarmDataService.page(page, wrapper);
+        Page<WaterAlarmOutDTO> outPage = new Page<>(pageNum, pageSize, result.getTotal());
+        outPage.setRecords(convertToVoList(result.getRecords()));
+        return outPage;
+    }
+
+    @Override
+    public WaterAlarmOutDTO getAlarmDetail(String alarmId) {
+        AlarmData alarm = alarmDataService.getById(alarmId);
+        if (alarm == null) {
+            throw new ServiceException("报警信息不存在");
+        }
+        List<WaterAlarmOutDTO> vos = convertToVoList(Collections.singletonList(alarm));
+        return vos.isEmpty() ? null : vos.get(0);
+    }
+
+    /**
+     * 报警记录 -> 出参(填充设备名称/位置/经纬度及关联工单)
+     */
+    private List<WaterAlarmOutDTO> convertToVoList(List<AlarmData> alarms) {
+        if (CollUtil.isEmpty(alarms)) {
+            return new ArrayList<>();
+        }
+        Map<String, EquipmentBase> eqMap = loadEquipmentCodeMap();
+        List<String> alarmIds = alarms.stream().map(AlarmData::getId).collect(Collectors.toList());
+        Map<String, WorkOrder> orderMap = loadWorkOrderMapByAlarmIds(alarmIds);
+        List<WaterAlarmOutDTO> vos = new ArrayList<>();
+        for (AlarmData alarm : alarms) {
+            WaterAlarmOutDTO vo = BeanUtil.copyProperties(alarm, WaterAlarmOutDTO.class);
+            EquipmentBase eq = alarm.getDeviceCode() == null ? null : eqMap.get(alarm.getDeviceCode());
+            if (eq != null) {
+                vo.setEquipmentId(eq.getEquipmentId());
+                if (StrUtil.isBlank(vo.getDeviceName())) {
+                    vo.setDeviceName(eq.getEquipmentName());
+                }
+                vo.setEquipmentLocation(eq.getEquipmentLocation());
+                if (eq.getLongitude() != null) {
+                    vo.setLongitude(eq.getLongitude().toPlainString());
+                }
+                if (eq.getLatitude() != null) {
+                    vo.setLatitude(eq.getLatitude().toPlainString());
+                }
+            }
+            vo.setAlarmLevelText(formatAlarmLevel(alarm.getAlarmLevel()));
+            vo.setAlarmStatusText(alarm.getAlarmStatus() != null && alarm.getAlarmStatus() == 0 ? "待处理" : "已处理");
+            WorkOrder order = alarm.getId() == null ? null : orderMap.get(alarm.getId());
+            if (order != null) {
+                vo.setOrderNo(order.getOrderNo());
+                vo.setOrderStatus(order.getOrderStatus());
+            }
+            vos.add(vo);
+        }
+        return vos;
+    }
+
+    private String formatAlarmLevel(Integer alarmLevel) {
+        if (alarmLevel == null) {
+            return "";
+        }
+        switch (alarmLevel) {
+            case 1:
+                return "一级报警";
+            case 2:
+                return "二级报警";
+            case 3:
+                return "三级报警";
+            case 4:
+                return "四级报警";
+            default:
+                return "报警";
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 处理 / 派发工单
+    // ------------------------------------------------------------------
+
+    @Override
+    public boolean resolveAlarm(String alarmId, String handleRemark) {
+        AlarmData alarm = alarmDataService.getById(alarmId);
+        if (alarm == null) {
+            throw new ServiceException("报警信息不存在");
+        }
+        alarm.setAlarmStatus(1);
+        alarm.setHandleTime(LocalDateTime.now());
+        alarm.setHandleUser(currentUserName());
+        alarm.setHandleRemark(StrUtil.blankToDefault(handleRemark, "标记已处理"));
+        alarm.setUpdateTime(LocalDateTime.now());
+        return alarmDataService.updateById(alarm);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean dispatchToWorkOrder(AlarmDispatchInDTO inDTO) {
+        if (inDTO == null || StrUtil.isBlank(inDTO.getAlarmId())) {
+            throw new ServiceException("报警ID不能为空");
+        }
+        AlarmData alarm = alarmDataService.getById(inDTO.getAlarmId());
+        if (alarm == null) {
+            throw new ServiceException("报警信息不存在");
+        }
+
+        // 设备信息(用于回填 deviceId/deviceCode)
+        EquipmentBase equipment = alarm.getDeviceCode() == null ? null
+                : equipmentBaseMapper.selectOne(new LambdaQueryWrapper<EquipmentBase>()
+                        .eq(EquipmentBase::getEquipmentCode, alarm.getDeviceCode())
+                        .last("LIMIT 1"));
+
+        WorkOrder workOrder = new WorkOrder();
+        workOrder.setOrderNo(generateOrderNo());
+        workOrder.setAlarmId(alarm.getId());
+        workOrder.setDeviceId(equipment != null ? equipment.getEquipmentId() : alarm.getDeviceCode());
+        workOrder.setDeviceCode(alarm.getDeviceCode());
+        workOrder.setOrderType(inDTO.getOrderType() == null ? 1 : inDTO.getOrderType());
+        workOrder.setOrderLevel(inDTO.getOrderLevel() == null
+                ? mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()) : inDTO.getOrderLevel());
+        workOrder.setOrderDesc(StrUtil.isNotBlank(inDTO.getOrderDesc()) ? inDTO.getOrderDesc()
+                : buildDefaultOrderDesc(alarm));
+        workOrder.setDeptId(inDTO.getDeptId());
+        workOrder.setPlanFinishTime(inDTO.getPlanFinishTime());
+        workOrder.setOrderStatus(1); // 1-待派单
+        workOrder.setCreateTime(new java.util.Date());
+        workOrder.setUpdateTime(new java.util.Date());
+        workOrderMapper.insert(workOrder);
+
+        // 写工单操作日志(1-创建工单)
+        WorkOrderLog workOrderLog = new WorkOrderLog();
+        workOrderLog.setOrderId(workOrder.getOrderId());
+        workOrderLog.setOperType(1);
+        workOrderLog.setOperDesc("由供水监测报警[" + alarm.getWarningType() + "]派发工单:" + workOrder.getOrderNo());
+        workOrderLog.setOperUserId(currentUserId());
+        workOrderLog.setOperUserName(currentUserName());
+        workOrderLog.setOperTime(new java.util.Date());
+        workOrderLogMapper.insert(workOrderLog);
+
+        // 回写报警处理信息
+        alarm.setAlarmStatus(1);
+        alarm.setHandleTime(LocalDateTime.now());
+        alarm.setHandleUser(currentUserName());
+        alarm.setHandleRemark("已派发工单:" + workOrder.getOrderNo());
+        alarm.setUpdateTime(LocalDateTime.now());
+        alarmDataService.updateById(alarm);
+        return true;
+    }
+
+    /**
+     * 报警等级 -> 工单优先级:1级→紧急,2/3级→一般,4级→低
+     */
+    private Integer mapAlarmLevelToOrderLevel(Integer alarmLevel) {
+        if (alarmLevel == null || alarmLevel == 1) {
+            return 1;
+        }
+        if (alarmLevel == 4) {
+            return 3;
+        }
+        return 2;
+    }
+
+    private String buildDefaultOrderDesc(AlarmData alarm) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(StrUtil.blankToDefault(alarm.getWarningType(), "监测报警"));
+        sb.append(",当前值:").append(alarm.getActualValue() == null ? "-" : alarm.getActualValue().toPlainString());
+        if (alarm.getMinValue() != null || alarm.getMaxValue() != null) {
+            sb.append(",阈值范围:").append(alarm.getMinValue() == null ? "-" : alarm.getMinValue().toPlainString())
+                    .append("~").append(alarm.getMaxValue() == null ? "-" : alarm.getMaxValue().toPlainString());
+        }
+        sb.append(",请及时现场核实处理");
+        return sb.toString();
+    }
+
+    /**
+     * 生成工单编号:WO + yyyyMMddHHmmss + 4位随机数
+     */
+    private String generateOrderNo() {
+        return "WO" + LocalDateTime.now().format(ORDER_NO_FMT)
+                + String.format("%04d", new Random().nextInt(10000));
+    }
+
+    // ------------------------------------------------------------------
+    // 报警审核 / 全流程跟踪
+    // ------------------------------------------------------------------
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AlarmAuditOutDTO auditAlarm(AlarmAuditInDTO inDTO) {
+        if (inDTO == null || StrUtil.isBlank(inDTO.getAlarmId())) {
+            throw new ServiceException("报警ID不能为空");
+        }
+        if (inDTO.getAuditAction() == null) {
+            throw new ServiceException("审核动作不能为空");
+        }
+        if (inDTO.getAuditResult() == null) {
+            throw new ServiceException("审核结果不能为空");
+        }
+        AlarmData alarm = alarmDataService.getById(inDTO.getAlarmId());
+        if (alarm == null) {
+            throw new ServiceException("报警信息不存在");
+        }
+
+        AlarmAudit audit = new AlarmAudit();
+        audit.setAlarmId(inDTO.getAlarmId());
+        audit.setAuditAction(inDTO.getAuditAction());
+        audit.setAuditResult(inDTO.getAuditResult());
+        audit.setAuditOpinion(inDTO.getAuditOpinion());
+        audit.setAttachUrl(inDTO.getAttachUrl());
+        audit.setAuditUserId(currentUserId());
+        audit.setAuditUserName(currentUserName());
+        audit.setAuditTime(LocalDateTime.now());
+        audit.setCreateTime(LocalDateTime.now());
+        audit.setUpdateTime(LocalDateTime.now());
+        alarmAuditMapper.insert(audit);
+
+        // 审核结果=误报 或 动作=解除 时,直接关闭报警
+        boolean releaseAlarm = inDTO.getAuditResult() == 2 || inDTO.getAuditAction() == 2;
+        if (releaseAlarm) {
+            alarm.setAlarmStatus(1);
+            alarm.setHandleTime(LocalDateTime.now());
+            alarm.setHandleUser(audit.getAuditUserName());
+            alarm.setHandleRemark("误报解除:" + StrUtil.blankToDefault(inDTO.getAuditOpinion(), "经审核确认为误报"));
+            alarm.setUpdateTime(LocalDateTime.now());
+            alarmDataService.updateById(alarm);
+        }
+        return toAuditOutDTO(audit);
+    }
+
+    @Override
+    public List<AlarmAuditOutDTO> getAuditList(String alarmId) {
+        List<AlarmAudit> audits = alarmAuditMapper.selectList(new LambdaQueryWrapper<AlarmAudit>()
+                .eq(AlarmAudit::getAlarmId, alarmId)
+                .orderByDesc(AlarmAudit::getAuditTime));
+        return audits.stream().map(this::toAuditOutDTO).collect(Collectors.toList());
+    }
+
+    @Override
+    public AlarmFlowOutDTO getAlarmFlow(String alarmId) {
+        AlarmFlowOutDTO flow = new AlarmFlowOutDTO();
+        flow.setAlarm(getAlarmDetail(alarmId));
+        flow.setAuditList(getAuditList(alarmId));
+        WorkOrder order = workOrderMapper.selectOne(new LambdaQueryWrapper<WorkOrder>()
+                .eq(WorkOrder::getAlarmId, alarmId)
+                .orderByDesc(WorkOrder::getOrderId)
+                .last("LIMIT 1"));
+        flow.setWorkOrder(order);
+        if (order != null) {
+            flow.setWorkOrderLogList(workOrderLogMapper.selectList(new LambdaQueryWrapper<WorkOrderLog>()
+                    .eq(WorkOrderLog::getOrderId, order.getOrderId())
+                    .orderByAsc(WorkOrderLog::getOperTime)));
+        }
+        return flow;
+    }
+
+    private AlarmAuditOutDTO toAuditOutDTO(AlarmAudit audit) {
+        AlarmAuditOutDTO vo = BeanUtil.copyProperties(audit, AlarmAuditOutDTO.class);
+        vo.setAuditActionText(audit.getAuditAction() != null && audit.getAuditAction() == 2 ? "解除" : "审核");
+        vo.setAuditResultText(audit.getAuditResult() != null && audit.getAuditResult() == 2 ? "误报" : "属实");
+        return vo;
+    }
+
+    // ------------------------------------------------------------------
+    // 监测报警一张图
+    // ------------------------------------------------------------------
+
+    @Override
+    public List<AlarmMapPointOutDTO> getAlarmMapPoints() {
+        Set<String> waterCodes = resolveWaterDeviceCodes();
+        if (waterCodes.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<AlarmData> pendingAlarms = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
+                .in(AlarmData::getDeviceCode, waterCodes)
+                .eq(AlarmData::getAlarmStatus, 0)
+                .orderByDesc(AlarmData::getAlarmTime));
+        Map<String, EquipmentBase> eqMap = loadEquipmentCodeMap();
+        // 同一设备只展示最新一条未处理报警
+        Map<String, AlarmData> latestPerDevice = new LinkedHashMap<>();
+        for (AlarmData alarm : pendingAlarms) {
+            latestPerDevice.putIfAbsent(alarm.getDeviceCode(), alarm);
+        }
+        List<AlarmMapPointOutDTO> points = new ArrayList<>();
+        for (AlarmData alarm : latestPerDevice.values()) {
+            EquipmentBase eq = alarm.getDeviceCode() == null ? null : eqMap.get(alarm.getDeviceCode());
+            if (eq == null || eq.getLongitude() == null || eq.getLatitude() == null) {
+                continue;
+            }
+            AlarmMapPointOutDTO point = new AlarmMapPointOutDTO();
+            point.setAlarmId(alarm.getId());
+            point.setDeviceCode(alarm.getDeviceCode());
+            point.setDeviceName(eq.getEquipmentName());
+            point.setEquipmentLocation(eq.getEquipmentLocation());
+            point.setLongitude(eq.getLongitude().toPlainString());
+            point.setLatitude(eq.getLatitude().toPlainString());
+            point.setWarningType(alarm.getWarningType());
+            point.setActualValue(alarm.getActualValue());
+            point.setAlarmLevel(alarm.getAlarmLevel());
+            point.setAlarmLevelText(formatAlarmLevel(alarm.getAlarmLevel()));
+            point.setAlarmTime(alarm.getAlarmTime());
+            points.add(point);
+        }
+        points.sort(Comparator.comparing(AlarmMapPointOutDTO::getAlarmLevel, Comparator.nullsLast(Comparator.naturalOrder())));
+        return points;
+    }
+
+    // ------------------------------------------------------------------
+    // 当前用户
+    // ------------------------------------------------------------------
+
+    private Long currentUserId() {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        return loginUser != null ? loginUser.getUserId() : null;
+    }
+
+    private String currentUserName() {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (loginUser == null) {
+            return "系统";
+        }
+        if (loginUser.getUser() != null && StrUtil.isNotBlank(loginUser.getUser().getNickName())) {
+            return loginUser.getUser().getNickName();
+        }
+        return loginUser.getUsername();
+    }
+}

+ 198 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyThresholdServiceImpl.java

@@ -0,0 +1,198 @@
+package com.zksy.WaterSupply.MonitorAlarm.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zksy.WaterSupply.MonitorAlarm.dto.in.WaterThresholdSaveInDTO;
+import com.zksy.WaterSupply.MonitorAlarm.service.IWaterSupplyThresholdService;
+import com.zksy.base.alarm.domain.WarningThreshold;
+import com.zksy.base.alarm.mapper.WarningThresholdMapper;
+import com.zksy.common.exception.ServiceException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 供水监测报警阈值业务实现
+ */
+@Slf4j
+@Service
+public class WaterSupplyThresholdServiceImpl extends ServiceImpl<WarningThresholdMapper, WarningThreshold>
+        implements IWaterSupplyThresholdService {
+
+    private static final Pattern TIME_PATTERN = Pattern.compile("^([01]\\d|2[0-3]):[0-5]\\d$");
+
+    @Override
+    public Page<WarningThreshold> findThresholdPage(long pageNum, long pageSize,
+                                                    String deviceCode, String warningType,
+                                                    String warningCode, String keyword) {
+        Page<WarningThreshold> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<WarningThreshold> wrapper = buildQueryWrapper(deviceCode, warningType, warningCode, keyword);
+        wrapper.orderByDesc(WarningThreshold::getUpdateTime);
+        return this.page(page, wrapper);
+    }
+
+    @Override
+    public List<WarningThreshold> findThresholdList(String deviceCode, String warningType, String warningCode) {
+        LambdaQueryWrapper<WarningThreshold> wrapper = buildQueryWrapper(deviceCode, warningType, warningCode, null);
+        wrapper.orderByDesc(WarningThreshold::getUpdateTime);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public WarningThreshold getThresholdDetail(String id) {
+        WarningThreshold threshold = this.getById(id);
+        if (threshold == null) {
+            throw new ServiceException("阈值信息不存在");
+        }
+        return threshold;
+    }
+
+    private LambdaQueryWrapper<WarningThreshold> buildQueryWrapper(String deviceCode, String warningType,
+                                                                   String warningCode, String keyword) {
+        LambdaQueryWrapper<WarningThreshold> wrapper = new LambdaQueryWrapper<>();
+        if (StrUtil.isNotBlank(deviceCode)) {
+            // device_code 支持多个设备编号逗号分隔
+            wrapper.apply("CONCAT(',', device_code, ',') LIKE CONCAT('%,', {0}, ',%')", deviceCode.trim());
+        }
+        wrapper.like(StrUtil.isNotBlank(warningType), WarningThreshold::getWarningType, warningType)
+                .like(StrUtil.isNotBlank(warningCode), WarningThreshold::getWarningCode, warningCode);
+        if (StrUtil.isNotBlank(keyword)) {
+            wrapper.and(w -> w.like(WarningThreshold::getDeviceCode, keyword)
+                    .or().like(WarningThreshold::getWarningType, keyword)
+                    .or().like(WarningThreshold::getWarningCode, keyword));
+        }
+        return wrapper;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int saveThresholdBatch(WaterThresholdSaveInDTO inDTO) {
+        if (inDTO == null || CollUtil.isEmpty(inDTO.getDeviceCodes())) {
+            throw new ServiceException("设备编码不能为空");
+        }
+        if (StrUtil.isBlank(inDTO.getWarningType()) || StrUtil.isBlank(inDTO.getWarningCode())) {
+            throw new ServiceException("预警类型和预警编码不能为空");
+        }
+        if (inDTO.getMinValue() == null && inDTO.getMaxValue() == null) {
+            throw new ServiceException("阈值最小值/最大值至少填写一项");
+        }
+        validatePeriod(inDTO.getPeriodStart(), inDTO.getPeriodEnd());
+
+        int count = 0;
+        List<String> deviceCodes = inDTO.getDeviceCodes().stream()
+                .filter(StrUtil::isNotBlank)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+        for (String deviceCode : deviceCodes) {
+            checkPeriodConflict(deviceCode, inDTO.getWarningCode(),
+                    inDTO.getPeriodStart(), inDTO.getPeriodEnd(), null);
+            WarningThreshold threshold = new WarningThreshold();
+            threshold.setDeviceCode(deviceCode);
+            threshold.setWarningType(inDTO.getWarningType().trim());
+            threshold.setWarningCode(inDTO.getWarningCode().trim());
+            threshold.setMinValue(inDTO.getMinValue());
+            threshold.setMaxValue(inDTO.getMaxValue());
+            threshold.setPeriodStart(inDTO.getPeriodStart());
+            threshold.setPeriodEnd(inDTO.getPeriodEnd());
+            threshold.setRemark(inDTO.getRemark());
+            threshold.setCreateTime(LocalDateTime.now());
+            threshold.setUpdateTime(LocalDateTime.now());
+            this.save(threshold);
+            count++;
+        }
+        return count;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateThreshold(WarningThreshold threshold) {
+        if (threshold == null || StrUtil.isBlank(threshold.getId())) {
+            throw new ServiceException("阈值ID不能为空");
+        }
+        WarningThreshold exist = this.getById(threshold.getId());
+        if (exist == null) {
+            throw new ServiceException("阈值信息不存在");
+        }
+        validatePeriod(threshold.getPeriodStart(), threshold.getPeriodEnd());
+        checkPeriodConflict(exist.getDeviceCode(), exist.getWarningCode(),
+                threshold.getPeriodStart(), threshold.getPeriodEnd(), exist.getId());
+
+        threshold.setCreateTime(exist.getCreateTime());
+        threshold.setUpdateTime(LocalDateTime.now());
+        return this.updateById(threshold);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteThresholdBatch(List<String> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            throw new ServiceException("请选择要删除的阈值");
+        }
+        return this.removeByIds(ids);
+    }
+
+    /**
+     * 校验分时时间格式与先后关系
+     */
+    private void validatePeriod(String periodStart, String periodEnd) {
+        boolean hasStart = StrUtil.isNotBlank(periodStart);
+        boolean hasEnd = StrUtil.isNotBlank(periodEnd);
+        if (!hasStart && !hasEnd) {
+            return; // 全天生效
+        }
+        if (!hasStart || !hasEnd) {
+            throw new ServiceException("分时阈值的生效开始时间和结束时间必须同时填写");
+        }
+        if (!TIME_PATTERN.matcher(periodStart.trim()).matches()
+                || !TIME_PATTERN.matcher(periodEnd.trim()).matches()) {
+            throw new ServiceException("分时时间格式必须为 HH:mm");
+        }
+        if (periodStart.trim().compareTo(periodEnd.trim()) >= 0) {
+            throw new ServiceException("分时阈值生效开始时间必须早于结束时间");
+        }
+    }
+
+    /**
+     * 校验同一设备+预警编码下分时时段不允许重叠
+     */
+    private void checkPeriodConflict(String deviceCode, String warningCode,
+                                     String periodStart, String periodEnd, String excludeId) {
+        LambdaQueryWrapper<WarningThreshold> wrapper = new LambdaQueryWrapper<>();
+        wrapper.apply("CONCAT(',', device_code, ',') LIKE CONCAT('%,', {0}, ',%')", deviceCode)
+                .eq(WarningThreshold::getWarningCode, warningCode);
+        if (StrUtil.isNotBlank(excludeId)) {
+            wrapper.ne(WarningThreshold::getId, excludeId);
+        }
+        List<WarningThreshold> exists = this.list(wrapper);
+        if (CollUtil.isEmpty(exists)) {
+            return;
+        }
+        for (WarningThreshold exist : exists) {
+            if (isPeriodOverlap(periodStart, periodEnd, exist.getPeriodStart(), exist.getPeriodEnd())) {
+                throw new ServiceException("设备[" + deviceCode + "]在相同预警编码[" + warningCode
+                        + "]下已存在重叠时段阈值,请调整分时设置");
+            }
+        }
+    }
+
+    /**
+     * 判断两个时段是否重叠(null 视为全天)
+     */
+    private boolean isPeriodOverlap(String start1, String end1, String start2, String end2) {
+        String s1 = StrUtil.isBlank(start1) ? "00:00" : start1.trim();
+        String e1 = StrUtil.isBlank(end1) ? "23:59" : end1.trim();
+        String s2 = StrUtil.isBlank(start2) ? "00:00" : start2.trim();
+        String e2 = StrUtil.isBlank(end2) ? "23:59" : end2.trim();
+        return s1.compareTo(e2) < 0 && s2.compareTo(e1) < 0;
+    }
+}

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

@@ -59,6 +59,20 @@ public class WarningThreshold implements Serializable {
     @ApiModelProperty(value = "预警最大值,大于等于该值视为报警")
     private Double maxValue;
 
+    /**
+     * 分时阈值生效开始时间(HH:mm),为空表示全天生效
+     */
+    @TableField(value = "period_start")
+    @ApiModelProperty(value = "分时阈值生效开始时间(HH:mm),为空表示全天生效")
+    private String periodStart;
+
+    /**
+     * 分时阈值生效结束时间(HH:mm),为空表示全天生效
+     */
+    @TableField(value = "period_end")
+    @ApiModelProperty(value = "分时阈值生效结束时间(HH:mm),为空表示全天生效")
+    private String periodEnd;
+
     /**
      * 备注
      */
@@ -82,4 +96,4 @@ public class WarningThreshold implements Serializable {
 
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;
-}
+}

+ 15 - 1
zk-api-service/src/main/java/com/zksy/api/domain/WarningThreshold.java

@@ -59,6 +59,20 @@ public class WarningThreshold implements Serializable {
     @ApiModelProperty(value = "预警最大值,大于等于该值视为报警")
     private Double maxValue;
 
+    /**
+     * 分时阈值生效开始时间(HH:mm),为空表示全天生效
+     */
+    @TableField(value = "period_start")
+    @ApiModelProperty(value = "分时阈值生效开始时间(HH:mm),为空表示全天生效")
+    private String periodStart;
+
+    /**
+     * 分时阈值生效结束时间(HH:mm),为空表示全天生效
+     */
+    @TableField(value = "period_end")
+    @ApiModelProperty(value = "分时阈值生效结束时间(HH:mm),为空表示全天生效")
+    private String periodEnd;
+
     /**
      * 备注
      */
@@ -82,4 +96,4 @@ public class WarningThreshold implements Serializable {
 
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;
-}
+}