merge: 合并 feature/issue-72 到 feature/dev (阈值管理+信息发布+设备管理)

- 合并冲突解决: 保留 issue-72 的完善版本(支持 MyBatis-Plus、AND/OR 组合条件引擎、逻辑删除)
- 覆盖 feature/dev 中的早期简化版 AlertRule 相关代码
- 新增: 阈值管理 CRUD + 信息发布 + 设备管理功能
This commit is contained in:
2026-06-14 16:02:50 +08:00
274 changed files with 18370 additions and 117 deletions
@@ -0,0 +1,212 @@
package com.water.production.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.result.R;
import com.water.production.entity.AlertNotification;
import com.water.production.entity.AlertRecord;
import com.water.production.entity.AlertRule;
import com.water.production.service.AlertCenterService;
import com.water.production.service.AlertRuleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 报警规则引擎 + 报警管理中心 REST API
*/
@Tag(name = "报警规则引擎与管理中心")
@RestController
@RequestMapping("/api/production/alert")
@RequiredArgsConstructor
public class AlertController {
private final AlertRuleService ruleService;
private final AlertCenterService centerService;
// ==================== 报警规则管理 (CRUD) ====================
@Operation(summary = "分页查询报警规则")
@GetMapping("/rule/page")
public R<Page<AlertRule>> rulePage(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String alertLevel,
@RequestParam(required = false) Boolean enabled) {
return R.ok(ruleService.page(current, size, keyword, alertLevel, enabled));
}
@Operation(summary = "查询规则详情")
@GetMapping("/rule/{id}")
public R<AlertRule> ruleDetail(@PathVariable Long id) {
AlertRule rule = ruleService.getById(id);
return rule != null ? R.ok(rule) : R.fail(404, "规则不存在");
}
@Operation(summary = "创建报警规则")
@PostMapping("/rule")
public R<AlertRule> createRule(@RequestBody AlertRule rule) {
return R.ok(ruleService.create(rule));
}
@Operation(summary = "更新报警规则")
@PutMapping("/rule/{id}")
public R<String> updateRule(@PathVariable Long id, @RequestBody AlertRule rule) {
rule.setId(id);
return ruleService.update(rule) ? R.ok("更新成功") : R.fail("更新失败");
}
@Operation(summary = "删除报警规则")
@DeleteMapping("/rule/{id}")
public R<String> deleteRule(@PathVariable Long id) {
return ruleService.delete(id) ? R.ok("删除成功") : R.fail("删除失败");
}
@Operation(summary = "启用/禁用规则")
@PutMapping("/rule/{id}/toggle")
public R<String> toggleRule(@PathVariable Long id, @RequestParam boolean enabled) {
return ruleService.toggleEnabled(id, enabled) ? R.ok("操作成功") : R.fail("操作失败");
}
@Operation(summary = "查询所有启用规则")
@GetMapping("/rule/enabled")
public R<List<AlertRule>> enabledRules() {
return R.ok(ruleService.findAllEnabled());
}
@Operation(summary = "规则等级统计")
@GetMapping("/rule/stats")
public R<List<Map<String, Object>>> ruleStats() {
return R.ok(ruleService.countByLevel());
}
// ==================== 规则引擎 - 指标评估 ====================
@Operation(summary = "提交指标数据触发规则评估")
@PostMapping("/evaluate")
public R<List<AlertRecord>> evaluate(
@RequestParam String deviceSn,
@RequestParam String metricKey,
@RequestParam double value,
@RequestParam(required = false) String area) {
return R.ok(ruleService.evaluateMetric(deviceSn, metricKey, value, area));
}
// ==================== 报警管理中心 ====================
@Operation(summary = "分页查询报警记录")
@GetMapping("/record/page")
public R<Page<AlertRecord>> recordPage(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String level,
@RequestParam(required = false) String area,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String deviceSn,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
return R.ok(centerService.page(current, size, level, area, status, deviceSn, startDate, endDate));
}
@Operation(summary = "查询报警详情(含通知记录)")
@GetMapping("/record/{id}")
public R<Map<String, Object>> recordDetail(@PathVariable Long id) {
Map<String, Object> detail = centerService.getDetail(id);
return detail != null ? R.ok(detail) : R.fail(404, "报警记录不存在");
}
@Operation(summary = "查询活跃报警")
@GetMapping("/record/active")
public R<List<AlertRecord>> activeAlerts() {
return R.ok(centerService.findActive());
}
@Operation(summary = "查询待确认报警")
@GetMapping("/record/pending")
public R<List<AlertRecord>> pendingAlerts() {
return R.ok(centerService.findPendingConfirmation());
}
@Operation(summary = "确认报警")
@PostMapping("/record/{id}/confirm")
public R<String> confirm(@PathVariable Long id, @RequestParam Long userId) {
return centerService.confirm(id, userId) ? R.ok("已确认") : R.fail("确认失败(状态不允许)");
}
@Operation(summary = "派单(指派处理人)")
@PostMapping("/record/{id}/dispatch")
public R<String> dispatch(@PathVariable Long id,
@RequestParam Long assigneeId,
@RequestParam(required = false) String assigneeName) {
return centerService.dispatch(id, assigneeId, assigneeName != null ? assigneeName : "")
? R.ok("已派单") : R.fail("派单失败(状态不允许)");
}
@Operation(summary = "开始处理")
@PostMapping("/record/{id}/start-handle")
public R<String> startHandle(@PathVariable Long id,
@RequestParam Long handlerId,
@RequestParam(required = false) String handlerName) {
return centerService.startHandle(id, handlerId, handlerName != null ? handlerName : "")
? R.ok("已开始处理") : R.fail("操作失败(状态不允许)");
}
@Operation(summary = "完成处理")
@PostMapping("/record/{id}/complete")
public R<String> completeHandle(@PathVariable Long id, @RequestBody Map<String, String> body) {
String result = body.getOrDefault("result", "");
return centerService.completeHandle(id, result) ? R.ok("处理完成") : R.fail("操作失败(状态不允许)");
}
@Operation(summary = "归档报警")
@PostMapping("/record/{id}/archive")
public R<String> archive(@PathVariable Long id, @RequestBody Map<String, String> body) {
String reason = body.getOrDefault("reason", "");
return centerService.archive(id, reason) ? R.ok("已归档") : R.fail("归档失败(状态不允许)");
}
@Operation(summary = "批量确认报警")
@PostMapping("/record/batch-confirm")
public R<Map<String, Object>> batchConfirm(@RequestBody Map<String, Object> body) {
@SuppressWarnings("unchecked")
List<Number> ids = (List<Number>) body.get("ids");
Long userId = ((Number) body.get("userId")).longValue();
List<Long> longIds = ids.stream().map(Number::longValue).toList();
int count = centerService.batchConfirm(longIds, userId);
return R.ok(Map.of("confirmed", count));
}
@Operation(summary = "批量归档报警")
@PostMapping("/record/batch-archive")
public R<Map<String, Object>> batchArchive(@RequestBody Map<String, Object> body) {
@SuppressWarnings("unchecked")
List<Number> ids = (List<Number>) body.get("ids");
String reason = (String) body.getOrDefault("reason", "");
List<Long> longIds = ids.stream().map(Number::longValue).toList();
int count = centerService.batchArchive(longIds, reason);
return R.ok(Map.of("archived", count));
}
@Operation(summary = "查询报警通知记录")
@GetMapping("/record/{id}/notifications")
public R<List<AlertNotification>> notifications(@PathVariable Long id) {
return R.ok(centerService.getNotifications(id));
}
// ==================== 统计看板 ====================
@Operation(summary = "报警统计看板")
@GetMapping("/dashboard")
public R<Map<String, Object>> dashboard(@RequestParam(defaultValue = "week") String period) {
return R.ok(centerService.getDashboard(period));
}
@Operation(summary = "今日报警概要")
@GetMapping("/today-summary")
public R<Map<String, Object>> todaySummary() {
return R.ok(centerService.getTodaySummary());
}
}
@@ -0,0 +1,106 @@
package com.water.production.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.water.common.core.result.R;
import com.water.production.entity.DispatchCommand;
import com.water.production.entity.DispatchExecution;
import com.water.production.entity.DispatchTracking;
import com.water.production.service.DispatchCommandService;
import com.water.production.service.DispatchTrackingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "调度指令管理")
@RestController
@RequestMapping("/api/production/dispatch-command")
@RequiredArgsConstructor
public class DispatchCommandController {
private final DispatchCommandService commandService;
private final DispatchTrackingService trackingService;
@Operation(summary = "创建指令")
@PostMapping
public R<DispatchCommand> create(@RequestBody DispatchCommand command) {
return R.ok(commandService.createCommand(command));
}
@Operation(summary = "下发指令")
@PostMapping("/{id}/issue")
public R<DispatchCommand> issue(@PathVariable Long id,
@RequestParam Long issuedBy,
@RequestParam(required = false, defaultValue = "system") String operatorName) {
return R.ok(commandService.issueCommand(id, issuedBy, operatorName));
}
@Operation(summary = "指令台账(分页)")
@GetMapping
public R<IPage<Map<String, Object>>> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String commandType,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
return R.ok(commandService.listCommands(page, size, status, commandType, keyword, startDate, endDate));
}
@Operation(summary = "指令详情")
@GetMapping("/{id}")
public R<Map<String, Object>> detail(@PathVariable Long id) {
return R.ok(commandService.getCommandDetail(id));
}
@Operation(summary = "各状态统计")
@GetMapping("/stats")
public R<List<Map<String, Object>>> stats() {
return R.ok(commandService.getStatusStats());
}
@Operation(summary = "接收确认")
@PostMapping("/{id}/receive")
public R<DispatchExecution> receive(@PathVariable Long id,
@RequestParam Long userId,
@RequestParam(required = false, defaultValue = "") String userName) {
return R.ok(commandService.receiveCommand(id, userId, userName));
}
@Operation(summary = "开始执行")
@PostMapping("/{id}/start-execute")
public R<DispatchExecution> startExecute(@PathVariable Long id,
@RequestParam Long userId,
@RequestParam(required = false, defaultValue = "") String userName) {
return R.ok(commandService.startExecution(id, userId, userName));
}
@Operation(summary = "完成执行")
@PostMapping("/{id}/complete")
public R<DispatchExecution> complete(@PathVariable Long id,
@RequestParam Long userId,
@RequestParam(required = false, defaultValue = "") String userName,
@RequestParam(required = false) String feedback,
@RequestParam(required = false) String feedbackImages) {
return R.ok(commandService.completeExecution(id, userId, userName, feedback, feedbackImages));
}
@Operation(summary = "驳回")
@PostMapping("/{id}/reject")
public R<DispatchExecution> reject(@PathVariable Long id,
@RequestParam Long userId,
@RequestParam(required = false, defaultValue = "") String userName,
@RequestParam String reason) {
return R.ok(commandService.rejectExecution(id, userId, userName, reason));
}
@Operation(summary = "查询追踪日志")
@GetMapping("/{id}/tracking")
public R<List<DispatchTracking>> trackingLogs(@PathVariable Long id) {
return R.ok(trackingService.getTrackingLogs(id));
}
}
@@ -0,0 +1,127 @@
package com.water.production.controller;
import com.water.common.core.result.R;
import com.water.production.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Tag(name = "供水生产管理")
@RestController
@RequestMapping("/production")
@RequiredArgsConstructor
public class ProductionController {
private final DashboardService dashboardService;
private final WaterQualityService wqService;
private final AlertEngine alertEngine;
private final DispatchService dispatchService;
private final DataCenterService dataCenterService;
private final VideoService videoService;
// ---- 总览 ----
@GetMapping("/overview")
public R<Map<String, Object>> overview(@RequestParam(defaultValue = "一体化水厂") String area,
@RequestParam(defaultValue = "admin") String roleType) {
return R.ok(dashboardService.getOverview(area, roleType));
}
// ---- 实时监测 ----
@GetMapping("/monitor/realtime")
public R<List<Map<String, Object>>> realtime(@RequestParam(required = false) String area,
@RequestParam(required = false) String positionType,
@RequestParam(required = false) String deviceType) {
return R.ok(dashboardService.getRealtimeMonitoring(area, positionType, deviceType));
}
@GetMapping("/monitor/cameras")
public R<List<Map<String, Object>>> cameras(@RequestParam String area) {
return R.ok(videoService.getCameras(area));
}
// ---- 水质 ----
@GetMapping("/quality/chemical/{station}")
public R<Map<String, Object>> chemical(@PathVariable String station) {
return R.ok(wqService.getChemicalMonitoring(station));
}
@PostMapping("/quality/record")
public R<String> addRecord(@RequestBody Map<String, Object> record) {
wqService.addRecord(record);
return R.ok("记录已保存");
}
@GetMapping("/quality/ledger")
public R<List<Map<String, Object>>> ledger(@RequestParam String area, @RequestParam String start, @RequestParam String end) {
return R.ok(wqService.getQualityLedger(area, start, end));
}
// ---- 报警 ----
@GetMapping("/alert/list")
public R<List<Map<String, Object>>> alerts(@RequestParam(required = false) String level,
@RequestParam(required = false) String area,
@RequestParam(defaultValue = "true") boolean active) {
return R.ok(alertEngine.getAlerts(level, area, active));
}
@PostMapping("/alert/{id}/confirm")
public R<String> confirm(@PathVariable Long id, @RequestParam Long userId) {
alertEngine.confirm(id, userId); return R.ok("已确认");
}
@PostMapping("/alert/{id}/dispatch")
public R<String> dispatch(@PathVariable Long id, @RequestParam Long assigneeId) {
alertEngine.dispatch(id, assigneeId); return R.ok("已派单");
}
// ---- 调度 ----
@GetMapping("/dispatch/duty/today")
public R<List<Map<String, Object>>> todayDuty(@RequestParam String area) {
return R.ok(dispatchService.getTodayDuty(area));
}
@PostMapping("/dispatch/command")
public R<Map<String, Object>> createCommand(@RequestBody Map<String, Object> req) {
@SuppressWarnings("unchecked")
List<Long> targetIds = (List<Long>) req.getOrDefault("targetIds", List.of());
return R.ok(dispatchService.createCommand(
(String) req.get("title"), (String) req.get("content"), (String) req.get("type"),
(String) req.get("source"), (String) req.get("targetType"), targetIds));
}
@PostMapping("/dispatch/command/{cmdNo}/issue")
public R<Map<String, Object>> issueCommand(@PathVariable String cmdNo) {
return R.ok(dispatchService.issueCommand(cmdNo));
}
@PostMapping("/dispatch/emergency/pipe-burst")
public R<Map<String, Object>> pipeBurst(@RequestBody Map<String, Object> req) {
return R.ok(dispatchService.pipeBurstSimulation(
((Number) req.get("lng")).doubleValue(),
((Number) req.get("lat")).doubleValue(),
(String) req.get("pipeDiameter")));
}
// ---- 数据中心 ----
@GetMapping("/data/history")
public R<List<Map<String, Object>>> history(@RequestParam String dataType, @RequestParam String area,
@RequestParam String start, @RequestParam String end) {
return R.ok(dataCenterService.getHistoryData(dataType, area, start, end));
}
@GetMapping("/data/report")
public R<Map<String, Object>> report(@RequestParam String type, @RequestParam String period) {
return R.ok(dataCenterService.generateReport(type, period));
}
@PutMapping("/data/threshold/{ruleId}")
public R<String> updateThreshold(@PathVariable Long ruleId, @RequestBody Map<String, Object> req) {
dataCenterService.updateThreshold(ruleId,
((Number) req.get("threshold")).doubleValue(),
(String) req.get("condition"));
return R.ok("阈值已更新");
}
}
@@ -0,0 +1,56 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 报警通知记录实体
*/
@Data
@TableName("prod_alert_notification")
public class AlertNotification {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联的报警记录ID */
private Long alertRecordId;
/** 关联的规则ID */
private Long ruleId;
/** 通知渠道: sms/wechat/app/email */
private String channel;
/** 接收人标识 */
private String recipient;
/** 接收人姓名 */
private String recipientName;
/** 通知标题 */
private String title;
/** 通知内容 */
private String content;
/** 状态: 0=待发送 1=已发送 2=发送失败 3=已读 */
private Integer status;
/** 发送时间 */
private LocalDateTime sendTime;
/** 阅读时间 */
private LocalDateTime readTime;
/** 重试次数 */
private Integer retryCount;
/** 错误信息 */
private String errorMsg;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
}
@@ -0,0 +1,106 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 报警记录实体(全生命周期)
* 状态流转: 0=活跃 → 1=已确认 → 2=已派单 → 3=处理中 → 4=已处理 → 5=已归档
*/
@Data
@TableName("prod_alert_record")
public class AlertRecord {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联规则ID */
private Long ruleId;
/** 规则名称 */
private String ruleName;
/** 设备ID */
private Long deviceId;
/** 设备编号 */
private String deviceSn;
/** 设备名称 */
private String deviceName;
/** 所属区域 */
private String area;
/** 指标键 */
private String metricKey;
/** 指标值 */
private BigDecimal metricValue;
/** 阈值(字符串,用于展示) */
private String thresholdValue;
/** 报警等级: general/important/urgent */
private String alertLevel;
/** 报警标题 */
private String title;
/** 报警详情 */
private String message;
/**
* 生命周期状态:
* 0=活跃, 1=已确认, 2=已派单, 3=处理中, 4=已处理, 5=已归档
*/
private Integer status;
/** 确认人ID */
private Long confirmedBy;
/** 确认时间 */
private LocalDateTime confirmedTime;
/** 派单时间 */
private LocalDateTime dispatchTime;
/** 指派人ID */
private Long assigneeId;
/** 指派人姓名 */
private String assigneeName;
/** 处理人ID */
private Long handlerId;
/** 处理人姓名 */
private String handlerName;
/** 处理结果 */
private String handleResult;
/** 处理时间 */
private LocalDateTime handleTime;
/** 归档时间 */
private LocalDateTime archiveTime;
/** 归档原因 */
private String archiveReason;
/** 解决时间 */
private LocalDateTime resolvedAt;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
@TableLogic
private Integer deleted;
}
@@ -1,25 +1,80 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* 报警规则定义实体
*/
@Data
@TableName("prod_alert_rule")
public class AlertRule {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String parameter;
private Double minValue;
private Double maxValue;
private Double warningValue;
private Double criticalValue;
private String conditionType;
private Integer severityLevel;
private Integer duplicateInterval;
private Integer status;
private String targetEquipmentId;
private String targetArea;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private String createBy;
private String updateBy;
}
/** 规则名称 */
private String ruleName;
/** 规则编码 */
private String ruleCode;
/** 规则描述 */
private String description;
/** 设备类型 */
private String deviceType;
/** 指标键 */
private String metricKey;
/** 报警等级: general/important/urgent */
private String alertLevel;
/** 条件表达式JSON (支持AND/OR组合) */
private String conditionExpr;
/** 简单阈值(向后兼容) */
private BigDecimal thresholdValue;
/** 去重窗口(秒) */
private Integer debounceSec;
/** 通知渠道(sms,wechat,app,email) */
private String notifyChannels;
/** 通知模板 */
private String notifyTemplate;
/** 是否启用 */
private Integer enabled;
/** 规则优先级 */
private Integer priority;
/** 生效开始时间 */
private LocalTime effectiveStart;
/** 生效结束时间 */
private LocalTime effectiveEnd;
/** 创建人 */
private Long createdBy;
/** 更新人 */
private Long updatedBy;
/** 逻辑删除 */
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -0,0 +1,11 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("prod_device_status")
public class DeviceStatus {
@TableId(type = IdType.AUTO) private Long id;
private String deviceId, deviceName, deviceType;
private String area; private Integer status; // 0离线 1在线 2故障
private Double lng, lat;
private LocalDateTime lastOnlineTime;
}
@@ -0,0 +1,64 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 调度指令主表
*/
@Data
@TableName("prod_dispatch_command")
public class DispatchCommand {
@TableId(type = IdType.AUTO)
private Long id;
/** 指令编号 CMD-yyyyMMddHHmmss-xxxx */
private String commandNo;
/** 指令标题 */
private String commandTitle;
/** 指令内容 */
private String commandContent;
/** 类型: normal/emergency/maintenance/inspection */
private String commandType;
/** 来源 */
private String source;
/** 优先级: low/normal/high/urgent */
private String priority;
/** 目标类型: user/dept/role */
private String targetType;
/** 目标ID列表 JSON数组 */
private String targetIds;
/** 状态: draft/issued/received/executing/completed/rejected */
private String status;
/** 下发时间 */
private LocalDateTime issuedAt;
/** 下发人 */
private Long issuedBy;
/** 完成归档时间 */
private LocalDateTime completedAt;
/** 备注 */
private String remark;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,52 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 调度指令执行记录表
*/
@Data
@TableName("prod_dispatch_execution")
public class DispatchExecution {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联指令ID */
private Long commandId;
/** 接收/执行人 */
private Long userId;
/** 执行人姓名 */
private String userName;
/** 接收确认时间 */
private LocalDateTime receivedAt;
/** 执行状态: pending/received/executing/completed/rejected */
private String executeStatus;
/** 执行反馈 */
private String feedback;
/** 反馈图片JSON数组 */
private String feedbackImages;
/** 完成时间 */
private LocalDateTime completedAt;
/** 驳回原因 */
private String rejectedReason;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,43 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 调度指令过程追踪日志表
*/
@Data
@TableName("prod_dispatch_tracking")
public class DispatchTracking {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联指令ID */
private Long commandId;
/** 关联执行记录ID(可选) */
private Long executionId;
/** 操作类型: create/issue/receive/start_execute/complete/reject/cancel */
private String action;
/** 操作人 */
private Long operatorId;
/** 操作人姓名 */
private String operatorName;
/** 原状态 */
private String fromStatus;
/** 新状态 */
private String toStatus;
/** 备注 */
private String remark;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
}
@@ -0,0 +1,11 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("prod_flow_pressure")
public class FlowPressureMonitor {
@TableId(type = IdType.AUTO) private Long id;
private String deviceId, area, positionType; // 流量/压力/液位
private Double value; private String unit;
private Double lng, lat;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
}
@@ -0,0 +1,10 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data @TableName("prod_video_camera")
public class VideoCamera {
@TableId(type = IdType.AUTO) private Long id;
private String cameraId, name, area;
private String streamUrl; private Integer status; // 0离线 1在线
private Double lng, lat;
}
@@ -0,0 +1,11 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("prod_water_quality")
public class WaterQualityRecord {
@TableId(type = IdType.AUTO) private Long id;
private String station, positionType; // 原水/出厂水/末梢水
private Double turbidity, ph, residualChlorine, color, odor;
private String result; // 合格/不合格
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
}
@@ -0,0 +1,27 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.AlertNotification;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface AlertNotificationMapper extends BaseMapper<AlertNotification> {
/**
* 按报警记录ID查询通知记录
*/
@Select("SELECT * FROM prod_alert_notification WHERE alert_record_id = #{recordId} ORDER BY created_time")
List<AlertNotification> findByAlertRecordId(@Param("recordId") Long recordId);
/**
* 统计通知发送情况
*/
@Select("SELECT channel, status, COUNT(*) as count FROM prod_alert_notification " +
"WHERE created_time >= #{startTime} GROUP BY channel, status")
List<Map<String, Object>> statisticsByChannel(@Param("startTime") String startTime);
}
@@ -0,0 +1,99 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.AlertRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
import java.util.Map;
@Mapper
public interface AlertRecordMapper extends BaseMapper<AlertRecord> {
/**
* 按状态统计报警数量
*/
@Select("SELECT status, COUNT(*) as count FROM prod_alert_record WHERE deleted = 0 GROUP BY status")
List<Map<String, Object>> countByStatus();
/**
* 按等级统计报警数量
*/
@Select("SELECT alert_level, COUNT(*) as count FROM prod_alert_record WHERE deleted = 0 " +
"AND created_time >= #{startTime} GROUP BY alert_level")
List<Map<String, Object>> countByLevel(@Param("startTime") String startTime);
/**
* 按区域统计报警数量
*/
@Select("SELECT area, COUNT(*) as count FROM prod_alert_record WHERE deleted = 0 " +
"AND created_time >= #{startTime} GROUP BY area ORDER BY count DESC")
List<Map<String, Object>> countByArea(@Param("startTime") String startTime);
/**
* 按日期统计报警趋势(最近N天)
*/
@Select("SELECT DATE(created_time) as date, alert_level, COUNT(*) as count " +
"FROM prod_alert_record WHERE deleted = 0 AND created_time >= #{startTime} " +
"GROUP BY DATE(created_time), alert_level ORDER BY date")
List<Map<String, Object>> trendByDate(@Param("startTime") String startTime);
/**
* 统计今日各状态报警数量
*/
@Select("SELECT status, alert_level, COUNT(*) as count FROM prod_alert_record " +
"WHERE deleted = 0 AND created_time >= CURRENT_DATE GROUP BY status, alert_level")
List<Map<String, Object>> todayStatistics();
/**
* 查询活跃报警(未归档)
*/
@Select("SELECT * FROM prod_alert_record WHERE deleted = 0 AND status < 5 ORDER BY created_time DESC")
List<AlertRecord> findActive();
/**
* 查询需要确认的报警
*/
@Select("SELECT * FROM prod_alert_record WHERE deleted = 0 AND status = 0 ORDER BY " +
"CASE alert_level WHEN 'urgent' THEN 1 WHEN 'important' THEN 2 ELSE 3 END, created_time DESC")
List<AlertRecord> findPendingConfirmation();
/**
* 确认报警
*/
@Update("UPDATE prod_alert_record SET status = 1, confirmed_by = #{userId}, " +
"confirmed_time = NOW(), updated_time = NOW() WHERE id = #{id} AND status = 0")
int confirm(@Param("id") Long id, @Param("userId") Long userId);
/**
* 派单
*/
@Update("UPDATE prod_alert_record SET status = 2, assignee_id = #{assigneeId}, " +
"assignee_name = #{assigneeName}, dispatch_time = NOW(), updated_time = NOW() " +
"WHERE id = #{id} AND status IN (0, 1)")
int dispatch(@Param("id") Long id, @Param("assigneeId") Long assigneeId, @Param("assigneeName") String assigneeName);
/**
* 开始处理
*/
@Update("UPDATE prod_alert_record SET status = 3, handler_id = #{handlerId}, " +
"handler_name = #{handlerName}, updated_time = NOW() WHERE id = #{id} AND status = 2")
int startHandle(@Param("id") Long id, @Param("handlerId") Long handlerId, @Param("handlerName") String handlerName);
/**
* 完成处理
*/
@Update("UPDATE prod_alert_record SET status = 4, handle_result = #{result}, " +
"handle_time = NOW(), resolved_at = NOW(), updated_time = NOW() WHERE id = #{id} AND status = 3")
int completeHandle(@Param("id") Long id, @Param("result") String result);
/**
* 归档
*/
@Update("UPDATE prod_alert_record SET status = 5, archive_time = NOW(), " +
"archive_reason = #{reason}, updated_time = NOW() WHERE id = #{id} AND status = 4")
int archive(@Param("id") Long id, @Param("reason") String reason);
}
@@ -1,39 +1,32 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.AlertRule;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface AlertRuleMapper {
// 查询所有报警规则
List<AlertRule> findAll();
// 根据ID查询报警规则
AlertRule findById(@Param("id") Long id);
// 根据参数查询报警规则
List<AlertRule> findByParameter(@Param("parameter") String parameter);
// 根据设备ID查询相关报警规则
List<AlertRule> findByTargetEquipmentId(@Param("targetEquipmentId") String targetEquipmentId);
// 根据区域查询报警规则
List<AlertRule> findByTargetArea(@Param("targetArea") String targetArea);
// 根据状态查询报警规则
List<AlertRule> findByStatus(@Param("status") Integer status);
// 插入报警规则
int insert(AlertRule alertRule);
// 更新报警规则
int update(AlertRule alertRule);
// 删除报警规则
int deleteById(@Param("id") Long id);
// 启用/禁用报警规则
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
}
public interface AlertRuleMapper extends BaseMapper<AlertRule> {
/**
* 根据指标键查询启用的规则
*/
@Select("SELECT * FROM prod_alert_rule WHERE metric_key = #{metricKey} AND enabled = 1 AND deleted = 0")
List<AlertRule> findEnabledByMetricKey(@Param("metricKey") String metricKey);
/**
* 查询所有启用的规则
*/
@Select("SELECT * FROM prod_alert_rule WHERE enabled = 1 AND deleted = 0 ORDER BY priority DESC")
List<AlertRule> findAllEnabled();
/**
* 统计各等级规则数量
*/
@Select("SELECT alert_level, COUNT(*) as count FROM prod_alert_rule WHERE enabled = 1 AND deleted = 0 GROUP BY alert_level")
List<Map<String, Object>> countByLevel();
}
@@ -0,0 +1,5 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.DeviceStatus;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface DeviceStatusMapper extends BaseMapper<DeviceStatus> {}
@@ -0,0 +1,28 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.production.entity.DispatchCommand;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {
IPage<Map<String, Object>> selectCommandPage(
Page<?> page,
@Param("status") String status,
@Param("commandType") String commandType,
@Param("keyword") String keyword,
@Param("startDate") String startDate,
@Param("endDate") String endDate
);
Map<String, Object> selectCommandDetail(@Param("commandId") Long commandId);
List<Map<String, Object>> selectStatusStats();
}
@@ -0,0 +1,9 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.DispatchExecution;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DispatchExecutionMapper extends BaseMapper<DispatchExecution> {
}
@@ -0,0 +1,9 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.DispatchTracking;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DispatchTrackingMapper extends BaseMapper<DispatchTracking> {
}
@@ -0,0 +1,5 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.FlowPressureMonitor;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface FlowPressureMonitorMapper extends BaseMapper<FlowPressureMonitor> {}
@@ -0,0 +1,5 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.VideoCamera;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface VideoCameraMapper extends BaseMapper<VideoCamera> {}
@@ -0,0 +1,5 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.WaterQualityRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface WaterQualityRecordMapper extends BaseMapper<WaterQualityRecord> {}
@@ -0,0 +1,309 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.production.entity.AlertNotification;
import com.water.production.entity.AlertRecord;
import com.water.production.mapper.AlertNotificationMapper;
import com.water.production.mapper.AlertRecordMapper;
import com.water.production.mapper.AlertRuleMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 报警管理中心服务 - 报警全生命周期管理 + 统计看板
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AlertCenterService {
private final AlertRecordMapper recordMapper;
private final AlertNotificationMapper notificationMapper;
private final AlertRuleMapper ruleMapper;
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// ==================== 报警列表与详情 ====================
/**
* 分页查询报警列表 (支持多条件筛选)
*/
public Page<AlertRecord> page(int current, int size, String level, String area,
Integer status, String deviceSn,
String startDate, String endDate) {
LambdaQueryWrapper<AlertRecord> wrapper = new LambdaQueryWrapper<>();
if (level != null && !level.isEmpty()) wrapper.eq(AlertRecord::getAlertLevel, level);
if (area != null && !area.isEmpty()) wrapper.eq(AlertRecord::getArea, area);
if (status != null) wrapper.eq(AlertRecord::getStatus, status);
if (deviceSn != null && !deviceSn.isEmpty()) wrapper.like(AlertRecord::getDeviceSn, deviceSn);
if (startDate != null && !startDate.isEmpty()) {
wrapper.ge(AlertRecord::getCreatedTime, LocalDate.parse(startDate, DATE_FMT).atStartOfDay());
}
if (endDate != null && !endDate.isEmpty()) {
wrapper.le(AlertRecord::getCreatedTime, LocalDate.parse(endDate, DATE_FMT).plusDays(1).atStartOfDay());
}
wrapper.orderByDesc(AlertRecord::getCreatedTime);
return recordMapper.selectPage(new Page<>(current, size), wrapper);
}
/**
* 查询报警详情 (含通知记录)
*/
public Map<String, Object> getDetail(Long id) {
AlertRecord record = recordMapper.selectById(id);
if (record == null) return null;
Map<String, Object> detail = new LinkedHashMap<>();
detail.put("record", record);
detail.put("notifications", notificationMapper.findByAlertRecordId(id));
detail.put("statusLabel", statusLabel(record.getStatus()));
detail.put("levelLabel", levelLabel(record.getAlertLevel()));
return detail;
}
/**
* 查询活跃报警
*/
public List<AlertRecord> findActive() {
return recordMapper.findActive();
}
/**
* 查询待确认报警
*/
public List<AlertRecord> findPendingConfirmation() {
return recordMapper.findPendingConfirmation();
}
// ==================== 生命周期操作 ====================
/**
* 确认报警
*/
@Transactional
public boolean confirm(Long id, Long userId) {
int rows = recordMapper.confirm(id, userId);
if (rows > 0) {
log.info("Alert confirmed: id={}, userId={}", id, userId);
}
return rows > 0;
}
/**
* 派单(指派给处理人)
*/
@Transactional
public boolean dispatch(Long id, Long assigneeId, String assigneeName) {
int rows = recordMapper.dispatch(id, assigneeId, assigneeName);
if (rows > 0) {
log.info("Alert dispatched: id={}, assignee={}", id, assigneeName);
// 可选: 创建巡检任务
createPatrolTask(id, assigneeId);
}
return rows > 0;
}
/**
* 开始处理
*/
@Transactional
public boolean startHandle(Long id, Long handlerId, String handlerName) {
int rows = recordMapper.startHandle(id, handlerId, handlerName);
if (rows > 0) {
log.info("Alert handling started: id={}, handler={}", id, handlerName);
}
return rows > 0;
}
/**
* 完成处理
*/
@Transactional
public boolean completeHandle(Long id, String result) {
int rows = recordMapper.completeHandle(id, result);
if (rows > 0) {
log.info("Alert handled: id={}", id);
}
return rows > 0;
}
/**
* 归档
*/
@Transactional
public boolean archive(Long id, String reason) {
int rows = recordMapper.archive(id, reason);
if (rows > 0) {
log.info("Alert archived: id={}", id);
}
return rows > 0;
}
/**
* 批量确认
*/
@Transactional
public int batchConfirm(List<Long> ids, Long userId) {
int count = 0;
for (Long id : ids) {
count += recordMapper.confirm(id, userId);
}
return count;
}
/**
* 批量归档
*/
@Transactional
public int batchArchive(List<Long> ids, String reason) {
int count = 0;
for (Long id : ids) {
count += recordMapper.archive(id, reason);
}
return count;
}
// ==================== 统计看板 ====================
/**
* 获取统计看板数据
*/
public Map<String, Object> getDashboard(String period) {
Map<String, Object> dashboard = new LinkedHashMap<>();
// 计算起始时间
String startTime = calculateStartTime(period);
// 总览数据
dashboard.put("totalByStatus", recordMapper.countByStatus());
dashboard.put("totalByLevel", recordMapper.countByLevel(startTime));
dashboard.put("totalByArea", recordMapper.countByArea(startTime));
dashboard.put("trend", recordMapper.trendByDate(startTime));
dashboard.put("today", recordMapper.todayStatistics());
// 规则统计
dashboard.put("ruleStats", ruleMapper.countByLevel());
// 通知统计
dashboard.put("notificationStats", notificationMapper.statisticsByChannel(startTime));
// 汇总数值
dashboard.put("summary", buildSummary(startTime));
dashboard.put("period", period);
dashboard.put("generatedAt", LocalDateTime.now());
return dashboard;
}
/**
* 获取今日报警概要
*/
public Map<String, Object> getTodaySummary() {
Map<String, Object> summary = new LinkedHashMap<>();
List<Map<String, Object>> todayStats = recordMapper.todayStatistics();
long totalToday = 0;
long activeCount = 0;
long confirmedCount = 0;
long handledCount = 0;
for (Map<String, Object> row : todayStats) {
long count = ((Number) row.get("count")).longValue();
int status = ((Number) row.get("status")).intValue();
totalToday += count;
if (status == 0) activeCount += count;
else if (status == 1) confirmedCount += count;
else if (status >= 4) handledCount += count;
}
summary.put("totalToday", totalToday);
summary.put("active", activeCount);
summary.put("confirmed", confirmedCount);
summary.put("handled", handledCount);
summary.put("pending", totalToday - handledCount);
summary.put("detail", todayStats);
return summary;
}
// ==================== 通知记录查询 ====================
/**
* 查询报警关联的通知记录
*/
public List<AlertNotification> getNotifications(Long alertRecordId) {
return notificationMapper.findByAlertRecordId(alertRecordId);
}
// ==================== 内部方法 ====================
private String calculateStartTime(String period) {
LocalDate now = LocalDate.now();
LocalDate start = switch (period != null ? period : "week") {
case "day" -> now.minusDays(1);
case "week" -> now.minusWeeks(1);
case "month" -> now.minusMonths(1);
case "quarter" -> now.minusMonths(3);
case "year" -> now.minusYears(1);
default -> now.minusWeeks(1);
};
return start.format(DATE_FMT);
}
private Map<String, Object> buildSummary(String startTime) {
Map<String, Object> summary = new LinkedHashMap<>();
// 总报警数
LambdaQueryWrapper<AlertRecord> totalWrapper = new LambdaQueryWrapper<>();
totalWrapper.ge(AlertRecord::getCreatedTime, LocalDate.parse(startTime, DATE_FMT).atStartOfDay());
long total = recordMapper.selectCount(totalWrapper);
summary.put("total", total);
// 活跃报警数
LambdaQueryWrapper<AlertRecord> activeWrapper = new LambdaQueryWrapper<>();
activeWrapper.lt(AlertRecord::getStatus, 5)
.ge(AlertRecord::getCreatedTime, LocalDate.parse(startTime, DATE_FMT).atStartOfDay());
summary.put("active", recordMapper.selectCount(activeWrapper));
// 紧急报警数
LambdaQueryWrapper<AlertRecord> urgentWrapper = new LambdaQueryWrapper<>();
urgentWrapper.eq(AlertRecord::getAlertLevel, "urgent")
.ge(AlertRecord::getCreatedTime, LocalDate.parse(startTime, DATE_FMT).atStartOfDay());
summary.put("urgent", recordMapper.selectCount(urgentWrapper));
return summary;
}
private void createPatrolTask(Long alertId, Long assigneeId) {
// 可选集成: 创建巡检任务关联报警
log.info("Patrol task creation for alert {} assigned to user {}", alertId, assigneeId);
}
private String statusLabel(Integer status) {
return switch (status) {
case 0 -> "活跃";
case 1 -> "已确认";
case 2 -> "已派单";
case 3 -> "处理中";
case 4 -> "已处理";
case 5 -> "已归档";
default -> "未知";
};
}
private String levelLabel(String level) {
return switch (level) {
case "urgent" -> "紧急";
case "important" -> "重要";
case "general" -> "一般";
default -> level;
};
}
}
@@ -0,0 +1,87 @@
package com.water.production.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
@RequiredArgsConstructor
public class AlertEngine {
private final JdbcTemplate jdbc;
private final Map<String, Long> lastAlertTime = new ConcurrentHashMap<>();
/** 检查指标是否触发报警 */
public void checkMetric(String deviceSn, String metricKey, double value, String area) {
List<Map<String, Object>> rules = jdbc.queryForList(
"SELECT * FROM alert_rule WHERE metric_key = ? AND enabled = 1", metricKey);
for (Map<String, Object> rule : rules) {
try {
String condition = (String) rule.get("condition_expr");
double threshold = ((Number) rule.get("threshold_value")).doubleValue();
String level = (String) rule.get("alert_level");
int debounce = ((Number) rule.get("debounce_sec")).intValue();
boolean triggered = false;
if (condition.startsWith(">")) triggered = value > threshold;
else if (condition.startsWith("<")) triggered = value < threshold;
else if (condition.startsWith(">=")) triggered = value >= threshold;
else if (condition.startsWith("<=")) triggered = value <= threshold;
if (!triggered) continue;
// 去重检查
String dedupKey = deviceSn + ":" + metricKey + ":" + level;
long now = Instant.now().getEpochSecond();
Long last = lastAlertTime.get(dedupKey);
if (last != null && (now - last) < debounce) continue;
lastAlertTime.put(dedupKey, now);
// 创建报警事件
Long ruleId = ((Number) rule.get("id")).longValue();
String message = String.format("%s %s: %.2f %s 阈值 %.2f",
deviceSn, metricKey, value, condition, threshold);
jdbc.update(
"INSERT INTO alert_event (rule_id, device_sn, area, metric_key, metric_value, threshold_value, alert_level, title, message) " +
"VALUES (?,?,?,?,?,?,?,?,?)",
ruleId, deviceSn, area, metricKey, value, String.valueOf(threshold), level,
"[" + level + "] " + metricKey + "异常", message);
log.info("Alert triggered: {} level={}", dedupKey, level);
} catch (Exception e) {
log.error("CheckMetric error: {}", e.getMessage());
}
}
}
/** 确认报警 */
public void confirm(Long alertId, Long userId) {
jdbc.update("UPDATE alert_event SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ?", userId, alertId);
}
/** 派单 */
public void dispatch(Long alertId, Long assigneeId) {
jdbc.update("UPDATE alert_event SET dispatched = 1 WHERE id = ?", alertId);
jdbc.update("INSERT INTO patrol_task (task_name, assignee_id, task_date, status) " +
"SELECT CONCAT('报警处理: ', title), ?, CURRENT_DATE, 'pending' FROM alert_event WHERE id = ?",
assigneeId, alertId);
}
/** 报警列表 */
public List<Map<String, Object>> getAlerts(String level, String area, boolean onlyActive) {
StringBuilder sql = new StringBuilder("SELECT * FROM alert_event WHERE 1=1");
if (level != null) sql.append(" AND alert_level = '").append(level).append("'");
if (area != null) sql.append(" AND area = '").append(area).append("'");
if (onlyActive) sql.append(" AND resolved_at IS NULL");
sql.append(" ORDER BY created_at DESC LIMIT 100");
return jdbc.queryForList(sql.toString());
}
}
@@ -1,39 +1,292 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.water.production.entity.AlertNotification;
import com.water.production.entity.AlertRecord;
import com.water.production.entity.AlertRule;
import java.util.List;
import com.water.production.mapper.AlertNotificationMapper;
import com.water.production.mapper.AlertRecordMapper;
import com.water.production.mapper.AlertRuleMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
public interface AlertRuleService {
// 查询所有报警规则
List<AlertRule> getAllAlertRules();
// 根据ID获取报警规则
AlertRule getAlertRuleById(Long id);
// 根据参数获取报警规则
List<AlertRule> getAlertRulesByParameter(String parameter);
// 根据设备获取报警规则
List<AlertRule> getAlertRulesByEquipment(String equipmentId);
// 根据区域获取报警规则
List<AlertRule> getAlertRulesByArea(String area);
// 创建报警规则
AlertRule createAlertRule(AlertRule alertRule);
// 更新报警规则
AlertRule updateAlertRule(AlertRule alertRule);
// 删除报警规则
boolean deleteAlertRule(Long id);
// 启用报警规则
boolean enableAlertRule(Long id);
// 禁用报警规则
boolean disableAlertRule(Long id);
// 验证报警规则
boolean validateAlertRule(AlertRule alertRule);
}
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 报警规则服务 - CRUD + 规则评估引擎(支持AND/OR组合条件)
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AlertRuleService {
private final AlertRuleMapper ruleMapper;
private final AlertRecordMapper recordMapper;
private final AlertNotificationMapper notificationMapper;
private final ObjectMapper objectMapper;
/** 去重缓存: key -> 上次触发时间戳(秒) */
private final Map<String, Long> lastTriggerTime = new ConcurrentHashMap<>();
// ==================== CRUD ====================
/**
* 分页查询规则
*/
public Page<AlertRule> page(int current, int size, String keyword, String alertLevel, Boolean enabled) {
LambdaQueryWrapper<AlertRule> wrapper = new LambdaQueryWrapper<>();
if (keyword != null && !keyword.isEmpty()) {
wrapper.and(w -> w.like(AlertRule::getRuleName, keyword)
.or().like(AlertRule::getRuleCode, keyword)
.or().like(AlertRule::getMetricKey, keyword));
}
if (alertLevel != null) wrapper.eq(AlertRule::getAlertLevel, alertLevel);
if (enabled != null) wrapper.eq(AlertRule::getEnabled, enabled ? 1 : 0);
wrapper.orderByDesc(AlertRule::getPriority).orderByDesc(AlertRule::getCreatedTime);
return ruleMapper.selectPage(new Page<>(current, size), wrapper);
}
/**
* 根据ID查询规则详情
*/
public AlertRule getById(Long id) {
return ruleMapper.selectById(id);
}
/**
* 创建规则
*/
@Transactional
public AlertRule create(AlertRule rule) {
rule.setCreatedTime(LocalDateTime.now());
rule.setUpdatedTime(LocalDateTime.now());
rule.setDeleted(0);
if (rule.getDebounceSec() == null) rule.setDebounceSec(300);
if (rule.getPriority() == null) rule.setPriority(0);
if (rule.getEnabled() == null) rule.setEnabled(1);
ruleMapper.insert(rule);
return rule;
}
/**
* 更新规则
*/
@Transactional
public boolean update(AlertRule rule) {
rule.setUpdatedTime(LocalDateTime.now());
return ruleMapper.updateById(rule) > 0;
}
/**
* 删除规则(逻辑删除)
*/
@Transactional
public boolean delete(Long id) {
return ruleMapper.deleteById(id) > 0;
}
/**
* 启用/禁用规则
*/
@Transactional
public boolean toggleEnabled(Long id, boolean enabled) {
AlertRule rule = new AlertRule();
rule.setId(id);
rule.setEnabled(enabled ? 1 : 0);
rule.setUpdatedTime(LocalDateTime.now());
return ruleMapper.updateById(rule) > 0;
}
/**
* 查询所有启用的规则
*/
public List<AlertRule> findAllEnabled() {
return ruleMapper.findAllEnabled();
}
/**
* 统计各等级规则数量
*/
public List<Map<String, Object>> countByLevel() {
return ruleMapper.countByLevel();
}
// ==================== 规则评估引擎 ====================
/**
* 评估指标值是否触发报警 (核心引擎)
*/
@Transactional
public List<AlertRecord> evaluateMetric(String deviceSn, String metricKey, double value, String area) {
List<AlertRule> rules = ruleMapper.findEnabledByMetricKey(metricKey);
List<AlertRecord> triggeredRecords = new ArrayList<>();
for (AlertRule rule : rules) {
try {
if (!isWithinEffectiveTime(rule)) continue;
boolean triggered = evaluateCondition(rule.getConditionExpr(), metricKey, value);
if (!triggered) continue;
String dedupKey = deviceSn + ":" + metricKey + ":" + rule.getAlertLevel();
long now = Instant.now().getEpochSecond();
Long lastTime = lastTriggerTime.get(dedupKey);
if (lastTime != null && (now - lastTime) < rule.getDebounceSec()) continue;
lastTriggerTime.put(dedupKey, now);
AlertRecord record = createAlertRecord(rule, deviceSn, metricKey, value, area);
recordMapper.insert(record);
triggeredRecords.add(record);
sendNotifications(rule, record);
log.info("Alert triggered: rule={} device={} metric={} value={} level={}",
rule.getRuleName(), deviceSn, metricKey, value, rule.getAlertLevel());
} catch (Exception e) {
log.error("Evaluate rule error [{}]: {}", rule.getRuleCode(), e.getMessage(), e);
}
}
return triggeredRecords;
}
public boolean evaluateCondition(String conditionExpr, String metricKey, double value) {
if (conditionExpr == null || conditionExpr.isEmpty()) return false;
try {
JsonNode root = objectMapper.readTree(conditionExpr);
if (root.has("op") && root.has("conditions")) {
return evaluateCompositeCondition(root, metricKey, value);
}
return evaluateSimpleCondition(root, metricKey, value);
} catch (Exception e) {
log.error("Parse condition expression error: {}", conditionExpr, e);
return false;
}
}
private boolean evaluateCompositeCondition(JsonNode root, String metricKey, double value) {
String op = root.get("op").asText().toUpperCase();
JsonNode conditions = root.get("conditions");
if (conditions == null || !conditions.isArray()) return false;
if ("AND".equals(op)) {
for (JsonNode condition : conditions) {
if (!evaluateSimpleCondition(condition, metricKey, value)) return false;
}
return true;
} else if ("OR".equals(op)) {
for (JsonNode condition : conditions) {
if (evaluateSimpleCondition(condition, metricKey, value)) return true;
}
return false;
}
return false;
}
private boolean evaluateSimpleCondition(JsonNode condition, String metricKey, double value) {
String condMetric = condition.has("metric") ? condition.get("metric").asText() : metricKey;
if (!condMetric.equals(metricKey)) return false;
String operator = condition.get("operator").asText();
double threshold = condition.get("threshold").asDouble();
return switch (operator) {
case ">" -> value > threshold;
case ">=" -> value >= threshold;
case "<" -> value < threshold;
case "<=" -> value <= threshold;
case "==" -> Math.abs(value - threshold) < 0.0001;
case "!=" -> Math.abs(value - threshold) >= 0.0001;
default -> false;
};
}
private boolean isWithinEffectiveTime(AlertRule rule) {
if (rule.getEffectiveStart() == null || rule.getEffectiveEnd() == null) return true;
LocalTime now = LocalTime.now();
if (rule.getEffectiveStart().isBefore(rule.getEffectiveEnd())) {
return !now.isBefore(rule.getEffectiveStart()) && !now.isAfter(rule.getEffectiveEnd());
} else {
return !now.isBefore(rule.getEffectiveStart()) || !now.isAfter(rule.getEffectiveEnd());
}
}
private AlertRecord createAlertRecord(AlertRule rule, String deviceSn, String metricKey, double value, String area) {
AlertRecord record = new AlertRecord();
record.setRuleId(rule.getId());
record.setRuleName(rule.getRuleName());
record.setDeviceSn(deviceSn);
record.setArea(area);
record.setMetricKey(metricKey);
record.setMetricValue(BigDecimal.valueOf(value));
record.setThresholdValue(rule.getThresholdValue() != null ? rule.getThresholdValue().toPlainString() : "");
record.setAlertLevel(rule.getAlertLevel());
record.setTitle(String.format("[%s] %s - %s", levelLabel(rule.getAlertLevel()), rule.getRuleName(), deviceSn));
record.setMessage(buildAlertMessage(rule, deviceSn, metricKey, value));
record.setStatus(0);
record.setCreatedTime(LocalDateTime.now());
record.setUpdatedTime(LocalDateTime.now());
record.setDeleted(0);
return record;
}
private String buildAlertMessage(AlertRule rule, String deviceSn, String metricKey, double value) {
return String.format("设备 %s 指标 %s 当前值 %.4f,触发规则: %s (等级: %s)\n规则描述: %s",
deviceSn, metricKey, value, rule.getRuleName(),
levelLabel(rule.getAlertLevel()),
rule.getDescription() != null ? rule.getDescription() : "无");
}
private void sendNotifications(AlertRule rule, AlertRecord record) {
String channels = rule.getNotifyChannels();
if (channels == null || channels.isEmpty()) return;
for (String channel : channels.split(",")) {
channel = channel.trim();
if (channel.isEmpty()) continue;
AlertNotification notification = new AlertNotification();
notification.setAlertRecordId(record.getId());
notification.setRuleId(rule.getId());
notification.setChannel(channel);
notification.setRecipient("system");
notification.setTitle(record.getTitle());
notification.setContent(record.getMessage());
notification.setStatus(0);
notification.setCreatedTime(LocalDateTime.now());
notificationMapper.insert(notification);
try {
doSendNotification(channel, notification);
notification.setStatus(1);
notification.setSendTime(LocalDateTime.now());
} catch (Exception e) {
notification.setStatus(2);
notification.setErrorMsg(e.getMessage());
log.error("Send notification error: channel={} record={}", channel, record.getId(), e);
}
notificationMapper.updateById(notification);
}
}
private void doSendNotification(String channel, AlertNotification notification) {
log.info("Sending alert notification: channel={}, title={}, recipient={}",
channel, notification.getTitle(), notification.getRecipient());
}
private String levelLabel(String level) {
return switch (level) {
case "urgent" -> "紧急";
case "important" -> "重要";
case "general" -> "一般";
default -> level;
};
}
}
@@ -0,0 +1,61 @@
package com.water.production.service;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
@RequiredArgsConstructor
public class DashboardService {
private final JdbcTemplate jdbc;
/** 获取供水总览数据(按角色自动定位区域) */
public Map<String, Object> getOverview(String area, String roleType) {
Map<String, Object> overview = new LinkedHashMap<>();
// 今日进出水量(从时序库聚合)
try {
Map<String, Object> flow = jdbc.queryForMap(
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
"FROM iot_telemetry WHERE ts >= CURRENT_DATE AND area = ?", area);
overview.put("todayInflow", flow.get("inflow"));
overview.put("todayOutflow", flow.get("outflow"));
} catch (Exception e) { overview.put("todayInflow", 0); overview.put("todayOutflow", 0); }
// 昨日供水量
overview.put("yesterdaySupply", jdbc.queryForObject(
"SELECT COALESCE(SUM(consumption),0) FROM rev_reading WHERE reading_date = CURRENT_DATE - 1", Double.class));
// 实时报警数
overview.put("activeAlerts", jdbc.queryForObject(
"SELECT COUNT(*) FROM alert_event WHERE confirmed_by IS NULL AND created_at >= CURRENT_DATE", Long.class));
// 设备运行概况
overview.put("deviceStats", jdbc.queryForList(
"SELECT status, COUNT(*) as count FROM iot_device WHERE area = ? GROUP BY status", area));
// 能耗药耗
overview.put("energy", Map.of("power_kwh", 1250.5, "pump_runtime_h", 18.2));
overview.put("chemical", Map.of("coagulant_kg", 45.0, "disinfectant_kg", 12.5));
overview.put("area", area);
overview.put("timestamp", System.currentTimeMillis());
return overview;
}
/** 实时监测列表(多维度筛选) */
public List<Map<String, Object>> getRealtimeMonitoring(String area, String positionType, String deviceType) {
StringBuilder sql = new StringBuilder(
"SELECT id, device_sn, device_name, device_type, position_type, area, status, last_report_time," +
"ST_X(geom) as lng, ST_Y(geom) as lat FROM iot_device WHERE 1=1");
if (area != null) sql.append(" AND area = '").append(area).append("'");
if (positionType != null) sql.append(" AND position_type = '").append(positionType).append("'");
if (deviceType != null) sql.append(" AND device_type = '").append(deviceType).append("'");
sql.append(" ORDER BY last_report_time DESC LIMIT 100");
return jdbc.queryForList(sql.toString());
}
}
@@ -0,0 +1,66 @@
package com.water.production.service;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
@RequiredArgsConstructor
public class DataCenterService {
private final JdbcTemplate jdbc;
/** 历史数据查看(多类型) */
public List<Map<String, Object>> getHistoryData(String dataType, String area, String startTime, String endTime) {
String table = switch (dataType) {
case "water_flow" -> "rev_reading";
case "water_quality" -> "water_quality_record";
case "alerts" -> "alert_event";
default -> "iot_telemetry";
};
return jdbc.queryForList(
"SELECT * FROM " + table + " WHERE (area = ? OR ? IS NULL) AND created_at BETWEEN ? AND ? LIMIT 500",
area, area, startTime, endTime);
}
/** 报表生成 */
public Map<String, Object> generateReport(String reportType, String period) {
Map<String, Object> report = new LinkedHashMap<>();
report.put("reportType", reportType);
report.put("period", period);
report.put("generatedAt", new Date());
switch (reportType) {
case "water_volume" -> report.put("data", jdbc.queryForList(
"SELECT area, SUM(consumption) as total FROM rev_reading WHERE reading_period = ? GROUP BY area", period));
case "water_quality" -> report.put("data", jdbc.queryForList(
"SELECT area, AVG(turbidity) as avg_turbidity, AVG(ph) as avg_ph, " +
"AVG(residual_chlorine) as avg_cl, COUNT(*) as tests, " +
"SUM(CASE WHEN is_qualified=1 THEN 1 ELSE 0 END)*100.0/NULLIF(COUNT(*),0) as pass_rate " +
"FROM water_quality_record WHERE to_char(test_date,'YYYY-MM') = ? GROUP BY area", period));
case "alert" -> report.put("data", jdbc.queryForList(
"SELECT alert_level, area, COUNT(*) as count FROM alert_event WHERE to_char(created_at,'YYYY-MM') = ? GROUP BY alert_level, area", period));
}
return report;
}
/** 阈值管理 */
public List<Map<String, Object>> getThresholds() {
return jdbc.queryForList("SELECT * FROM alert_rule WHERE enabled = 1 ORDER BY device_type, metric_key");
}
public void updateThreshold(Long ruleId, double newThreshold, String newCondition) {
jdbc.update("UPDATE alert_rule SET threshold_value = ?, condition_expr = ? WHERE id = ?",
newThreshold, newCondition, ruleId);
}
/** 信息发布 */
public void publishInfo(String type, String title, String content) {
jdbc.update(
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = ?",
title, content, "info_release_" + type);
}
}
@@ -0,0 +1,228 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.exception.BusinessException;
import com.water.production.entity.DispatchCommand;
import com.water.production.entity.DispatchExecution;
import com.water.production.entity.DispatchTracking;
import com.water.production.mapper.DispatchCommandMapper;
import com.water.production.mapper.DispatchExecutionMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class DispatchCommandService {
private final DispatchCommandMapper commandMapper;
private final DispatchExecutionMapper executionMapper;
private final DispatchTrackingService trackingService;
private static final Map<String, Set<String>> STATE_TRANSITIONS = new LinkedHashMap<>();
static {
STATE_TRANSITIONS.put("draft", Set.of("issued"));
STATE_TRANSITIONS.put("issued", Set.of("received", "rejected"));
STATE_TRANSITIONS.put("received", Set.of("executing", "rejected"));
STATE_TRANSITIONS.put("executing", Set.of("completed", "rejected"));
STATE_TRANSITIONS.put("completed", Set.of());
STATE_TRANSITIONS.put("rejected", Set.of());
}
@Transactional
public DispatchCommand createCommand(DispatchCommand command) {
String cmdNo = "CMD-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+ "-" + String.format("%04d", new Random().nextInt(10000));
command.setCommandNo(cmdNo);
command.setStatus("draft");
commandMapper.insert(command);
trackingService.log(command.getId(), null, "create", null, null, "draft", "创建指令");
log.info("创建调度指令: {}", cmdNo);
return command;
}
@Transactional
public DispatchCommand issueCommand(Long commandId, Long issuedBy, String operatorName) {
DispatchCommand cmd = getCommandOrThrow(commandId);
validateTransition(cmd.getStatus(), "issued");
cmd.setStatus("issued");
cmd.setIssuedAt(LocalDateTime.now());
cmd.setIssuedBy(issuedBy);
commandMapper.updateById(cmd);
createExecutionRecords(cmd);
trackingService.log(commandId, null, "issue", issuedBy, operatorName, "draft", "issued", "指令下发");
log.info("下发调度指令: {}", cmd.getCommandNo());
return cmd;
}
@Transactional
public DispatchExecution receiveCommand(Long commandId, Long userId, String userName) {
DispatchCommand cmd = getCommandOrThrow(commandId);
validateTransition(cmd.getStatus(), "received");
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
if (!Objects.equals(exec.getExecuteStatus(), "pending")) {
throw new BusinessException("该执行记录状态不允许接收确认");
}
exec.setExecuteStatus("received");
exec.setReceivedAt(LocalDateTime.now());
executionMapper.updateById(exec);
if (allExecutionsInStatus(commandId, "received")) {
cmd.setStatus("received");
commandMapper.updateById(cmd);
}
trackingService.log(commandId, exec.getId(), "receive", userId, userName, "pending", "received", "接收确认");
return exec;
}
@Transactional
public DispatchExecution startExecution(Long commandId, Long userId, String userName) {
DispatchCommand cmd = getCommandOrThrow(commandId);
validateTransition(cmd.getStatus(), "executing");
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
if (!Objects.equals(exec.getExecuteStatus(), "received")) {
throw new BusinessException("必须先接收确认才能开始执行");
}
exec.setExecuteStatus("executing");
executionMapper.updateById(exec);
if (Objects.equals(cmd.getStatus(), "received")) {
cmd.setStatus("executing");
commandMapper.updateById(cmd);
}
trackingService.log(commandId, exec.getId(), "start_execute", userId, userName, "received", "executing", "开始执行");
return exec;
}
@Transactional
public DispatchExecution completeExecution(Long commandId, Long userId, String userName,
String feedback, String feedbackImages) {
DispatchCommand cmd = getCommandOrThrow(commandId);
validateTransition(cmd.getStatus(), "completed");
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
if (!Objects.equals(exec.getExecuteStatus(), "executing")) {
throw new BusinessException("只有执行中状态才能完成");
}
exec.setExecuteStatus("completed");
exec.setFeedback(feedback);
exec.setFeedbackImages(feedbackImages);
exec.setCompletedAt(LocalDateTime.now());
executionMapper.updateById(exec);
if (allExecutionsFinal(commandId)) {
cmd.setStatus("completed");
cmd.setCompletedAt(LocalDateTime.now());
commandMapper.updateById(cmd);
trackingService.log(commandId, null, "complete", userId, userName, "executing", "completed", "全部执行完成,归档");
}
trackingService.log(commandId, exec.getId(), "complete", userId, userName, "executing", "completed", "执行完成");
return exec;
}
@Transactional
public DispatchExecution rejectExecution(Long commandId, Long userId, String userName, String reason) {
DispatchCommand cmd = getCommandOrThrow(commandId);
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
String prevStatus = exec.getExecuteStatus();
if (Objects.equals(prevStatus, "completed") || Objects.equals(prevStatus, "rejected")) {
throw new BusinessException("当前状态不允许驳回");
}
exec.setExecuteStatus("rejected");
exec.setRejectedReason(reason);
exec.setCompletedAt(LocalDateTime.now());
executionMapper.updateById(exec);
if (allExecutionsFinal(commandId)) {
cmd.setStatus("rejected");
commandMapper.updateById(cmd);
trackingService.log(commandId, null, "reject", userId, userName, cmd.getStatus(), "rejected", "全部驳回/终止");
}
trackingService.log(commandId, exec.getId(), "reject", userId, userName, prevStatus, "rejected", "驳回原因: " + reason);
return exec;
}
public IPage<Map<String, Object>> listCommands(int page, int size, String status, String commandType,
String keyword, String startDate, String endDate) {
return commandMapper.selectCommandPage(new Page<>(page, size), status, commandType, keyword, startDate, endDate);
}
public Map<String, Object> getCommandDetail(Long commandId) {
Map<String, Object> detail = commandMapper.selectCommandDetail(commandId);
if (detail == null) {
throw new BusinessException("指令不存在");
}
detail.put("trackingLogs", trackingService.getTrackingLogs(commandId));
return detail;
}
public List<Map<String, Object>> getStatusStats() {
return commandMapper.selectStatusStats();
}
private DispatchCommand getCommandOrThrow(Long commandId) {
DispatchCommand cmd = commandMapper.selectById(commandId);
if (cmd == null) throw new BusinessException("指令不存在");
return cmd;
}
private DispatchExecution getExecutionOrThrow(Long commandId, Long userId) {
LambdaQueryWrapper<DispatchExecution> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DispatchExecution::getCommandId, commandId)
.eq(DispatchExecution::getUserId, userId);
DispatchExecution exec = executionMapper.selectOne(wrapper);
if (exec == null) throw new BusinessException("执行记录不存在");
return exec;
}
private void validateTransition(String currentStatus, String targetStatus) {
Set<String> allowed = STATE_TRANSITIONS.get(currentStatus);
if (allowed == null || !allowed.contains(targetStatus)) {
throw new BusinessException("状态流转不合法: " + currentStatus + " -> " + targetStatus);
}
}
private void createExecutionRecords(DispatchCommand cmd) {
if (cmd.getTargetIds() == null || cmd.getTargetIds().isBlank()) return;
String cleaned = cmd.getTargetIds().replaceAll("[\\[\\]\"]", "");
for (String idStr : cleaned.split(",")) {
String trimmed = idStr.trim();
if (trimmed.isEmpty()) continue;
try {
Long userId = Long.parseLong(trimmed);
DispatchExecution exec = new DispatchExecution();
exec.setCommandId(cmd.getId());
exec.setUserId(userId);
exec.setExecuteStatus("pending");
executionMapper.insert(exec);
} catch (NumberFormatException e) {
log.warn("跳过无效目标ID: {}", trimmed);
}
}
}
private boolean allExecutionsInStatus(Long commandId, String status) {
LambdaQueryWrapper<DispatchExecution> w1 = new LambdaQueryWrapper<>();
w1.eq(DispatchExecution::getCommandId, commandId);
Long total = executionMapper.selectCount(w1);
LambdaQueryWrapper<DispatchExecution> w2 = new LambdaQueryWrapper<>();
w2.eq(DispatchExecution::getCommandId, commandId)
.eq(DispatchExecution::getExecuteStatus, status);
Long count = executionMapper.selectCount(w2);
return total > 0 && total.equals(count);
}
private boolean allExecutionsFinal(Long commandId) {
LambdaQueryWrapper<DispatchExecution> w1 = new LambdaQueryWrapper<>();
w1.eq(DispatchExecution::getCommandId, commandId);
Long total = executionMapper.selectCount(w1);
LambdaQueryWrapper<DispatchExecution> w2 = new LambdaQueryWrapper<>();
w2.eq(DispatchExecution::getCommandId, commandId)
.in(DispatchExecution::getExecuteStatus, "completed", "rejected");
Long finalCount = executionMapper.selectCount(w2);
return total > 0 && total.equals(finalCount);
}
}
@@ -0,0 +1,100 @@
package com.water.production.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class DispatchService {
private final JdbcTemplate jdbc;
// ========== 值班管理 ==========
public List<Map<String, Object>> getTodayDuty(String area) {
return jdbc.queryForList(
"SELECT dr.*, u.real_name, u.phone, ds.shift_type " +
"FROM duty_record dr JOIN sys_user u ON dr.user_id = u.id " +
"JOIN duty_schedule ds ON dr.schedule_id = ds.id " +
"WHERE dr.duty_date = CURRENT_DATE AND ds.status = 1");
}
public Map<String, Object> startDuty(Long userId) {
jdbc.update("UPDATE duty_record SET status = 'on_duty', on_duty_at = NOW() WHERE user_id = ? AND duty_date = CURRENT_DATE", userId);
return Map.of("status", "on_duty", "startedAt", new Date());
}
public Map<String, Object> endDuty(Long userId, String handoverRemark) {
jdbc.update(
"UPDATE duty_record SET status = 'off_duty', off_duty_at = NOW(), handover_remark = ? WHERE user_id = ? AND duty_date = CURRENT_DATE",
handoverRemark, userId);
return Map.of("status", "off_duty");
}
// ========== 调度指令 ==========
public Map<String, Object> createCommand(String title, String content, String type, String source, String targetType, List<Long> targetIds) {
String cmdNo = "CMD-" + System.currentTimeMillis();
jdbc.update(
"INSERT INTO dispatch_command (command_no, command_type, command_title, command_content, source, target_type, target_ids, status) " +
"VALUES (?,?,?,?,?,?,?::jsonb,'draft')",
cmdNo, type, title, content, source, targetType, targetIds.toString());
log.info("Command created: {} type={}", cmdNo, type);
return Map.of("commandNo", cmdNo, "status", "draft");
}
public Map<String, Object> issueCommand(String cmdNo) {
jdbc.update("UPDATE dispatch_command SET status = 'issued', issued_at = NOW() WHERE command_no = ?", cmdNo);
// 记录日志
jdbc.update("INSERT INTO dispatch_log (command_id, action) SELECT id, 'issue' FROM dispatch_command WHERE command_no = ?", cmdNo);
return Map.of("commandNo", cmdNo, "status", "issued");
}
public Map<String, Object> trackCommand(String cmdNo) {
return jdbc.queryForMap("SELECT * FROM dispatch_command WHERE command_no = ?", cmdNo);
}
public List<Map<String, Object>> getCommandLog(String cmdNo) {
return jdbc.queryForList(
"SELECT dl.* FROM dispatch_log dl JOIN dispatch_command dc ON dl.command_id = dc.id WHERE dc.command_no = ? ORDER BY dl.created_at",
cmdNo);
}
// ========== 应急调度推演 ==========
public Map<String, Object> pipeBurstSimulation(double lng, double lat, String pipeDiameter) {
// 爆管模拟:影响区域分析
Map<String, Object> result = new LinkedHashMap<>();
result.put("scenario", "爆管");
result.put("location", Map.of("lng", lng, "lat", lat));
result.put("pipeDiameter", pipeDiameter);
result.put("affectedArea", "半径500m");
result.put("affectedCustomers", 230);
result.put("suggestedActions", List.of(
"关闭上游阀门 V-001, V-002",
"启动应急供水方案 B",
"通知受影响用户(短信+公告)",
"调度抢修队出发"
));
result.put("estimatedRecoveryHours", 4);
return result;
}
public Map<String, Object> waterQualityIncident(String area, String pollutant) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("scenario", "水质异常");
result.put("area", area);
result.put("pollutant", pollutant);
result.put("suggestedActions", List.of(
"立即停止该片区供水",
"启动备用水源",
"水质采样送检",
"向下游水厂发出预警"
));
result.put("riskLevel", "critical");
return result;
}
}
@@ -0,0 +1,53 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.production.entity.DispatchTracking;
import com.water.production.mapper.DispatchTrackingMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class DispatchTrackingService {
private final DispatchTrackingMapper trackingMapper;
public void log(Long commandId, Long executionId, String action,
Long operatorId, String operatorName,
String fromStatus, String toStatus, String remark) {
DispatchTracking tracking = new DispatchTracking();
tracking.setCommandId(commandId);
tracking.setExecutionId(executionId);
tracking.setAction(action);
tracking.setOperatorId(operatorId);
tracking.setOperatorName(operatorName);
tracking.setFromStatus(fromStatus);
tracking.setToStatus(toStatus);
tracking.setRemark(remark);
trackingMapper.insert(tracking);
}
public void log(Long commandId, Long executionId, String action,
Long operatorId, String operatorName,
String toStatus, String remark) {
log(commandId, executionId, action, operatorId, operatorName, null, toStatus, remark);
}
public List<DispatchTracking> getTrackingLogs(Long commandId) {
LambdaQueryWrapper<DispatchTracking> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DispatchTracking::getCommandId, commandId)
.orderByAsc(DispatchTracking::getCreatedAt);
return trackingMapper.selectList(wrapper);
}
public List<DispatchTracking> getExecutionTrackingLogs(Long executionId) {
LambdaQueryWrapper<DispatchTracking> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DispatchTracking::getExecutionId, executionId)
.orderByAsc(DispatchTracking::getCreatedAt);
return trackingMapper.selectList(wrapper);
}
}
@@ -0,0 +1,34 @@
package com.water.production.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
@Slf4j
@Service
public class VideoService {
/** 获取所有视频监控点位 */
public List<Map<String, Object>> getCameras(String area) {
// Mock: 返回预设视频点位
List<Map<String, Object>> cameras = new ArrayList<>();
cameras.add(Map.of("id", 1, "name", "一体化水厂-沉淀池", "rtsp", "rtsp://192.168.1.100/stream1", "area", "一体化水厂", "status", "online"));
cameras.add(Map.of("id", 2, "name", "查村调压站-入口", "rtsp", "rtsp://192.168.1.101/stream1", "area", "八家户片区", "status", "online"));
cameras.add(Map.of("id", 3, "name", "精芒片区-管网节点1", "rtsp", "rtsp://192.168.1.102/stream1", "area", "精芒片区", "status", "online"));
return cameras;
}
/** AI 人员闯入检测 */
public Map<String, Object> detectIntrusion(String cameraId, byte[] frameData) {
// Mock: YOLOv8 推理 (实际调用模型服务)
double probability = Math.random();
boolean intruder = probability > 0.85;
if (intruder) {
log.warn("Intrusion detected on camera {} (prob={})", cameraId, String.format("%.2f", probability));
return Map.of("cameraId", cameraId, "intruder", true, "confidence", probability,
"alert", "检测到人员闯入", "timestamp", System.currentTimeMillis());
}
return Map.of("cameraId", cameraId, "intruder", false, "confidence", probability);
}
}
@@ -0,0 +1,61 @@
package com.water.production.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class WaterQualityService {
private final JdbcTemplate jdbc;
/** 药剂投加监控:全工艺参数 */
public Map<String, Object> getChemicalMonitoring(String stationName) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("station", stationName);
// 混凝
data.put("inflowTurbidity", Map.of("value", 12.5, "unit", "NTU", "status", "normal"));
data.put("coagulantRate", Map.of("value", 25.3, "unit", "mg/L", "status", "normal"));
// 沉淀
data.put("sedimentationLevel", Map.of("value", 3.2, "unit", "m", "status", "normal"));
data.put("sedimentationTurbidity", Map.of("value", 3.1, "unit", "NTU", "status", "normal"));
// 过滤
data.put("filterLevel", Map.of("value", 2.5, "unit", "m", "status", "normal"));
data.put("filterHeadLoss", Map.of("value", 0.8, "unit", "m", "status", "normal"));
// 消毒
data.put("disinfectantRate", Map.of("value", 2.0, "unit", "mg/L", "status", "normal"));
data.put("residualChlorine", Map.of("value", 0.5, "unit", "mg/L", "status", "normal"));
data.put("outflowTurbidity", Map.of("value", 0.3, "unit", "NTU", "status", "normal"));
return data;
}
/** 人工检测点位规划 */
public List<Map<String, Object>> getManualTestPoints(String area) {
return jdbc.queryForList(
"SELECT DISTINCT test_point, point_type, lng, lat FROM water_quality_record WHERE area = ? AND test_type = 'manual' ORDER BY test_point",
area);
}
/** 水质数据台账 */
public List<Map<String, Object>> getQualityLedger(String area, String startDate, String endDate) {
return jdbc.queryForList(
"SELECT * FROM water_quality_record WHERE area = ? AND test_date BETWEEN ? AND ? ORDER BY test_date DESC LIMIT 200",
area, startDate, endDate);
}
/** 添加检测记录 */
public void addRecord(Map<String, Object> record) {
jdbc.update(
"INSERT INTO water_quality_record (test_type, test_point, point_type, area, test_date, test_time, tester, turbidity, ph, residual_chlorine, is_qualified) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?)",
record.get("testType"), record.get("testPoint"), record.get("pointType"), record.get("area"),
record.get("testDate"), record.get("testTime"), record.get("tester"),
record.get("turbidity"), record.get("ph"), record.get("residualChlorine"),
record.get("isQualified"));
}
}
@@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS prod_water_quality (
id BIGSERIAL PRIMARY KEY, station VARCHAR(100), position_type VARCHAR(20),
turbidity DOUBLE PRECISION, ph DOUBLE PRECISION,
residual_chlorine DOUBLE PRECISION, color DOUBLE PRECISION, odor DOUBLE PRECISION,
result VARCHAR(20), created_time TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS prod_flow_pressure (
id BIGSERIAL PRIMARY KEY, device_id VARCHAR(50), area VARCHAR(50),
position_type VARCHAR(20), value DOUBLE PRECISION, unit VARCHAR(20),
lng DOUBLE PRECISION, lat DOUBLE PRECISION,
created_time TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS prod_device_status (
id BIGSERIAL PRIMARY KEY, device_id VARCHAR(50), device_name VARCHAR(100),
device_type VARCHAR(30), area VARCHAR(50), status INT DEFAULT 0,
lng DOUBLE PRECISION, lat DOUBLE PRECISION,
last_online_time TIMESTAMP
);
CREATE TABLE IF NOT EXISTS prod_alert_record (
id BIGSERIAL PRIMARY KEY, device_id VARCHAR(50), area VARCHAR(50),
level VARCHAR(10), alert_type VARCHAR(30), description TEXT,
status INT DEFAULT 0, confirmed_by BIGINT, handled_by BIGINT,
created_time TIMESTAMP DEFAULT NOW(),
confirmed_time TIMESTAMP, handled_time TIMESTAMP
);
CREATE TABLE IF NOT EXISTS prod_video_camera (
id BIGSERIAL PRIMARY KEY, camera_id VARCHAR(50), name VARCHAR(100),
area VARCHAR(50), stream_url VARCHAR(500), status INT DEFAULT 0,
lng DOUBLE PRECISION, lat DOUBLE PRECISION
);
@@ -0,0 +1,57 @@
-- 调度指令管理模块 DDL
CREATE TABLE IF NOT EXISTS prod_dispatch_command (
id BIGSERIAL PRIMARY KEY,
command_no VARCHAR(64) NOT NULL UNIQUE,
command_title VARCHAR(200) NOT NULL,
command_content TEXT NOT NULL,
command_type VARCHAR(32) NOT NULL,
source VARCHAR(100),
priority VARCHAR(16) DEFAULT 'normal',
target_type VARCHAR(32),
target_ids TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'draft',
issued_at TIMESTAMP,
issued_by BIGINT,
completed_at TIMESTAMP,
remark TEXT,
deleted INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS prod_dispatch_execution (
id BIGSERIAL PRIMARY KEY,
command_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
user_name VARCHAR(64),
received_at TIMESTAMP,
execute_status VARCHAR(32) DEFAULT 'pending',
feedback TEXT,
feedback_images TEXT,
completed_at TIMESTAMP,
rejected_reason TEXT,
deleted INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS prod_dispatch_tracking (
id BIGSERIAL PRIMARY KEY,
command_id BIGINT NOT NULL,
execution_id BIGINT,
action VARCHAR(32) NOT NULL,
operator_id BIGINT,
operator_name VARCHAR(64),
from_status VARCHAR(32),
to_status VARCHAR(32),
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_cmd_status ON prod_dispatch_command(status);
CREATE INDEX IF NOT EXISTS idx_cmd_type ON prod_dispatch_command(command_type);
CREATE INDEX IF NOT EXISTS idx_cmd_created ON prod_dispatch_command(created_at);
CREATE INDEX IF NOT EXISTS idx_exec_cmd ON prod_dispatch_execution(command_id);
CREATE INDEX IF NOT EXISTS idx_exec_user ON prod_dispatch_execution(user_id);
CREATE INDEX IF NOT EXISTS idx_track_cmd ON prod_dispatch_tracking(command_id);
@@ -0,0 +1,44 @@
<?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.water.production.mapper.DispatchCommandMapper">
<select id="selectCommandPage" resultType="java.util.Map">
SELECT
c.id, c.command_no, c.command_title, c.command_type,
c.source, c.priority, c.status, c.issued_at, c.created_at,
COUNT(e.id) AS total_executions,
COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS completed_count,
COUNT(CASE WHEN e.execute_status = 'rejected' THEN 1 END) AS rejected_count
FROM prod_dispatch_command c
LEFT JOIN prod_dispatch_execution e ON e.command_id = c.id AND e.deleted = 0
WHERE c.deleted = 0
<if test="status != null and status != ''">AND c.status = #{status}</if>
<if test="commandType != null and commandType != ''">AND c.command_type = #{commandType}</if>
<if test="keyword != null and keyword != ''">
AND (c.command_no LIKE '%' || #{keyword} || '%' OR c.command_title LIKE '%' || #{keyword} || '%')
</if>
<if test="startDate != null and startDate != ''">AND c.created_at &gt;= #{startDate}::timestamp</if>
<if test="endDate != null and endDate != ''">AND c.created_at &lt;= #{endDate}::timestamp</if>
GROUP BY c.id
ORDER BY c.created_at DESC
</select>
<select id="selectCommandDetail" resultType="java.util.Map">
SELECT c.*,
(SELECT json_agg(json_build_object(
'id', e.id, 'userId', e.user_id, 'userName', e.user_name,
'executeStatus', e.execute_status, 'receivedAt', e.received_at,
'feedback', e.feedback, 'completedAt', e.completed_at,
'rejectedReason', e.rejected_reason
)) FROM prod_dispatch_execution e WHERE e.command_id = c.id AND e.deleted = 0) AS executions
FROM prod_dispatch_command c
WHERE c.id = #{commandId} AND c.deleted = 0
</select>
<select id="selectStatusStats" resultType="java.util.Map">
SELECT status, COUNT(*) AS count
FROM prod_dispatch_command WHERE deleted = 0
GROUP BY status
</select>
</mapper>
@@ -0,0 +1,159 @@
package com.water.production.service;
import com.water.production.entity.AlertRecord;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* AlertCenterService 报警管理中心单元测试
* 测试状态流转、标签映射等核心逻辑
*/
class AlertCenterServiceTest {
@Test
@DisplayName("测试报警状态标签映射")
void testStatusLabelMapping() {
// 通过反射或构造测试状态标签的正确性
AlertRecord record = new AlertRecord();
// 测试所有状态值
record.setStatus(0);
assertEquals(0, record.getStatus());
record.setStatus(1);
assertEquals(1, record.getStatus());
record.setStatus(2);
assertEquals(2, record.getStatus());
record.setStatus(3);
assertEquals(3, record.getStatus());
record.setStatus(4);
assertEquals(4, record.getStatus());
record.setStatus(5);
assertEquals(5, record.getStatus());
}
@Test
@DisplayName("测试报警等级标签")
void testAlertLevelValues() {
AlertRecord record = new AlertRecord();
record.setAlertLevel("general");
assertEquals("general", record.getAlertLevel());
record.setAlertLevel("important");
assertEquals("important", record.getAlertLevel());
record.setAlertLevel("urgent");
assertEquals("urgent", record.getAlertLevel());
}
@Test
@DisplayName("测试报警记录生命周期字段完整性")
void testAlertRecordLifecycleFields() {
AlertRecord record = new AlertRecord();
record.setId(1L);
record.setRuleId(100L);
record.setRuleName("管网压力过高报警");
record.setDeviceSn("DEV-001");
record.setArea("城东片区");
record.setMetricKey("pressure");
record.setAlertLevel("urgent");
record.setTitle("[紧急] 管网压力过高报警 - DEV-001");
record.setMessage("设备 DEV-001 指标 pressure 当前值 0.95");
record.setStatus(0);
record.setConfirmedBy(null);
record.setConfirmedTime(null);
// 验证初始状态
assertEquals(0, record.getStatus());
assertNull(record.getConfirmedBy());
assertNull(record.getAssigneeId());
assertNull(record.getHandlerId());
assertNull(record.getHandleResult());
assertNull(record.getArchiveReason());
// 模拟确认
record.setStatus(1);
record.setConfirmedBy(10L);
assertEquals(1, record.getStatus());
assertEquals(10L, record.getConfirmedBy());
// 模拟派单
record.setStatus(2);
record.setAssigneeId(20L);
record.setAssigneeName("张三");
assertEquals(2, record.getStatus());
assertEquals("张三", record.getAssigneeName());
// 模拟处理中
record.setStatus(3);
record.setHandlerId(20L);
record.setHandlerName("张三");
assertEquals(3, record.getStatus());
// 模拟完成处理
record.setStatus(4);
record.setHandleResult("已更换压力传感器,恢复正常");
assertEquals(4, record.getStatus());
assertNotNull(record.getHandleResult());
// 模拟归档
record.setStatus(5);
record.setArchiveReason("处理完毕,确认无误");
assertEquals(5, record.getStatus());
assertNotNull(record.getArchiveReason());
}
@Test
@DisplayName("测试条件表达式JSON结构验证")
void testConditionExpressionStructure() {
// 验证AND条件JSON
String andExpr = "{\"op\":\"AND\",\"conditions\":[" +
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":1.5}" +
"]}";
assertTrue(andExpr.contains("\"op\":\"AND\""));
assertTrue(andExpr.contains("\"conditions\""));
// 验证OR条件JSON
String orExpr = "{\"op\":\"OR\",\"conditions\":[" +
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
"]}";
assertTrue(orExpr.contains("\"op\":\"OR\""));
}
@Test
@DisplayName("测试报警标题格式化")
void testAlertTitleFormat() {
String level = "urgent";
String ruleName = "管网压力过高报警";
String deviceSn = "DEV-001";
String levelLabel = switch (level) {
case "urgent" -> "紧急";
case "important" -> "重要";
case "general" -> "一般";
default -> level;
};
String title = String.format("[%s] %s - %s", levelLabel, ruleName, deviceSn);
assertEquals("[紧急] 管网压力过高报警 - DEV-001", title);
// 测试一般等级
levelLabel = switch ("general") {
case "urgent" -> "紧急";
case "important" -> "重要";
case "general" -> "一般";
default -> "general";
};
title = String.format("[%s] %s - %s", levelLabel, "余氯偏低报警", "DEV-002");
assertEquals("[一般] 余氯偏低报警 - DEV-002", title);
}
}
@@ -0,0 +1,143 @@
package com.water.production.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* AlertRuleService 规则评估引擎单元测试
* 测试条件表达式解析(AND/OR组合)、阈值触发、多级别报警
*/
class AlertRuleServiceTest {
private AlertRuleService ruleService;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
ruleService = new AlertRuleService(null, null, null, objectMapper);
}
@Test
@DisplayName("测试简单大于条件 - 触发报警")
void testSimpleGreaterThan_triggered() {
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
assertTrue(ruleService.evaluateCondition(expr, "pressure", 0.9));
}
@Test
@DisplayName("测试简单大于条件 - 未触发")
void testSimpleGreaterThan_notTriggered() {
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
assertFalse(ruleService.evaluateCondition(expr, "pressure", 0.7));
}
@Test
@DisplayName("测试简单小于条件 - 触发报警")
void testSimpleLessThan_triggered() {
String expr = "{\"metric\":\"residual_chlorine\",\"operator\":\"<\",\"threshold\":0.1}";
assertTrue(ruleService.evaluateCondition(expr, "residual_chlorine", 0.05));
}
@Test
@DisplayName("测试AND组合条件 - 全部满足时触发")
void testAndCondition_allMet() {
String expr = "{\"op\":\"AND\",\"conditions\":[" +
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":1.5}" +
"]}";
assertTrue(ruleService.evaluateCondition(expr, "pressure", 1.0));
}
@Test
@DisplayName("测试AND组合条件 - 部分不满足时不触发")
void testAndCondition_partialMet() {
String expr = "{\"op\":\"AND\",\"conditions\":[" +
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":0.9}" +
"]}";
assertFalse(ruleService.evaluateCondition(expr, "pressure", 1.0));
}
@Test
@DisplayName("测试OR组合条件 - 任一满足即触发")
void testOrCondition_oneMet() {
String expr = "{\"op\":\"OR\",\"conditions\":[" +
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
"]}";
// flow=3 < 5, 满足第二个条件
assertTrue(ruleService.evaluateCondition(expr, "flow", 3.0));
// flow=150 > 100, 满足第一个条件
assertTrue(ruleService.evaluateCondition(expr, "flow", 150.0));
}
@Test
@DisplayName("测试OR组合条件 - 全不满足时不触发")
void testOrCondition_noneMet() {
String expr = "{\"op\":\"OR\",\"conditions\":[" +
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
"]}";
// flow=50, 不满足任何条件
assertFalse(ruleService.evaluateCondition(expr, "flow", 50.0));
}
@Test
@DisplayName("测试不同指标键不匹配 - 不触发")
void testMetricKeyMismatch() {
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
// 条件检查pressure, 但实际metric是temperature
assertFalse(ruleService.evaluateCondition(expr, "temperature", 50.0));
}
@Test
@DisplayName("测试等于条件")
void testEqualsCondition() {
String expr = "{\"metric\":\"ph\",\"operator\":\"==\",\"threshold\":7.0}";
assertTrue(ruleService.evaluateCondition(expr, "ph", 7.0));
assertFalse(ruleService.evaluateCondition(expr, "ph", 7.1));
}
@Test
@DisplayName("测试大于等于和小于等于条件")
void testGreaterEqualAndLessEqual() {
String exprGte = "{\"metric\":\"pressure\",\"operator\":\">=\",\"threshold\":0.8}";
assertTrue(ruleService.evaluateCondition(exprGte, "pressure", 0.8));
assertTrue(ruleService.evaluateCondition(exprGte, "pressure", 0.9));
assertFalse(ruleService.evaluateCondition(exprGte, "pressure", 0.79));
String exprLte = "{\"metric\":\"pressure\",\"operator\":\"<=\",\"threshold\":0.2}";
assertTrue(ruleService.evaluateCondition(exprLte, "pressure", 0.2));
assertTrue(ruleService.evaluateCondition(exprLte, "pressure", 0.1));
assertFalse(ruleService.evaluateCondition(exprLte, "pressure", 0.21));
}
@Test
@DisplayName("测试空表达式和无效表达式")
void testNullAndInvalidExpression() {
assertFalse(ruleService.evaluateCondition(null, "pressure", 0.9));
assertFalse(ruleService.evaluateCondition("", "pressure", 0.9));
assertFalse(ruleService.evaluateCondition("not-json", "pressure", 0.9));
}
@Test
@DisplayName("测试多级别报警 - general/important/urgent 条件分别触发")
void testMultiLevelAlerts() {
// 一般: 余氯 < 0.1
String generalExpr = "{\"metric\":\"residual_chlorine\",\"operator\":\"<\",\"threshold\":0.1}";
assertTrue(ruleService.evaluateCondition(generalExpr, "residual_chlorine", 0.05));
// 重要: 压力 < 0.2
String importantExpr = "{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":0.2}";
assertTrue(ruleService.evaluateCondition(importantExpr, "pressure", 0.15));
// 紧急: 浊度 > 1.0
String urgentExpr = "{\"metric\":\"turbidity\",\"operator\":\">\",\"threshold\":1.0}";
assertTrue(ruleService.evaluateCondition(urgentExpr, "turbidity", 2.5));
}
}
@@ -0,0 +1,223 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.production.entity.DispatchCommand;
import com.water.production.entity.DispatchExecution;
import com.water.production.mapper.DispatchCommandMapper;
import com.water.production.mapper.DispatchExecutionMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DispatchCommandServiceTest {
@Mock
private DispatchCommandMapper commandMapper;
@Mock
private DispatchExecutionMapper executionMapper;
@Mock
private DispatchTrackingService trackingService;
@InjectMocks
private DispatchCommandService commandService;
@Test
void testCreateCommand() {
when(commandMapper.insert(any())).thenReturn(1);
DispatchCommand cmd = new DispatchCommand();
cmd.setCommandTitle("测试调度指令");
cmd.setCommandContent("请检查A区域管网压力");
cmd.setCommandType("normal");
cmd.setPriority("high");
cmd.setSource("手动");
cmd.setTargetType("user");
cmd.setTargetIds("[1,2]");
DispatchCommand result = commandService.createCommand(cmd);
assertNotNull(result.getCommandNo());
assertTrue(result.getCommandNo().startsWith("CMD-"));
assertEquals("draft", result.getStatus());
assertEquals("测试调度指令", result.getCommandTitle());
verify(commandMapper).insert(any());
verify(trackingService).log(any(), isNull(), eq("create"), isNull(), isNull(), eq("draft"), any());
}
@Test
void testIssueCommand() {
DispatchCommand cmd = buildCommand("draft");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
when(executionMapper.insert(any())).thenReturn(1);
DispatchCommand result = commandService.issueCommand(1L, 100L, "admin");
assertEquals("issued", result.getStatus());
assertNotNull(result.getIssuedAt());
assertEquals(100L, result.getIssuedBy());
verify(executionMapper, times(2)).insert(any()); // 2 target users
verify(trackingService).log(eq(1L), isNull(), eq("issue"), eq(100L), eq("admin"),
eq("draft"), eq("issued"), any());
}
@Test
void testIssueCommand_invalidTransition() {
DispatchCommand cmd = buildCommand("issued");
when(commandMapper.selectById(1L)).thenReturn(cmd);
assertThrows(BusinessException.class, () -> {
commandService.issueCommand(1L, 100L, "admin");
});
}
@Test
void testReceiveCommand() {
DispatchCommand cmd = buildCommand("issued");
when(commandMapper.selectById(1L)).thenReturn(cmd);
DispatchExecution exec = buildExecution("pending");
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
when(executionMapper.updateById(any())).thenReturn(1);
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
.thenReturn(2L) // total
.thenReturn(2L); // all received
DispatchExecution result = commandService.receiveCommand(1L, 1L, "张三");
assertEquals("received", result.getExecuteStatus());
assertNotNull(result.getReceivedAt());
}
@Test
void testStartExecution() {
DispatchCommand cmd = buildCommand("received");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
DispatchExecution exec = buildExecution("received");
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
when(executionMapper.updateById(any())).thenReturn(1);
DispatchExecution result = commandService.startExecution(1L, 1L, "张三");
assertEquals("executing", result.getExecuteStatus());
}
@Test
void testStartExecution_wrongStatus() {
DispatchCommand cmd = buildCommand("received");
when(commandMapper.selectById(1L)).thenReturn(cmd);
DispatchExecution exec = buildExecution("pending");
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
assertThrows(BusinessException.class, () -> {
commandService.startExecution(1L, 1L, "张三");
});
}
@Test
void testCompleteExecution() {
DispatchCommand cmd = buildCommand("executing");
when(commandMapper.selectById(1L)).thenReturn(cmd);
DispatchExecution exec = buildExecution("executing");
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
when(executionMapper.updateById(any())).thenReturn(1);
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
.thenReturn(1L) // total
.thenReturn(1L); // all final
DispatchExecution result = commandService.completeExecution(1L, 1L, "张三", "已完成巡检", null);
assertEquals("completed", result.getExecuteStatus());
assertEquals("已完成巡检", result.getFeedback());
assertNotNull(result.getCompletedAt());
}
@Test
void testCompleteExecution_wrongStatus() {
DispatchCommand cmd = buildCommand("executing");
when(commandMapper.selectById(1L)).thenReturn(cmd);
DispatchExecution exec = buildExecution("received");
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
assertThrows(BusinessException.class, () -> {
commandService.completeExecution(1L, 1L, "张三", "反馈", null);
});
}
@Test
void testRejectExecution() {
DispatchCommand cmd = buildCommand("issued");
when(commandMapper.selectById(1L)).thenReturn(cmd);
DispatchExecution exec = buildExecution("pending");
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
when(executionMapper.updateById(any())).thenReturn(1);
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
.thenReturn(1L) // total
.thenReturn(1L); // all final
DispatchExecution result = commandService.rejectExecution(1L, 1L, "张三", "人手不足");
assertEquals("rejected", result.getExecuteStatus());
assertEquals("人手不足", result.getRejectedReason());
}
@Test
void testRejectExecution_alreadyCompleted() {
DispatchCommand cmd = buildCommand("executing");
when(commandMapper.selectById(1L)).thenReturn(cmd);
DispatchExecution exec = buildExecution("completed");
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
assertThrows(BusinessException.class, () -> {
commandService.rejectExecution(1L, 1L, "张三", "原因");
});
}
@Test
void testCommandNotFound() {
when(commandMapper.selectById(999L)).thenReturn(null);
assertThrows(BusinessException.class, () -> {
commandService.issueCommand(999L, 1L, "admin");
});
}
// ==================== Helper ====================
private DispatchCommand buildCommand(String status) {
DispatchCommand cmd = new DispatchCommand();
cmd.setId(1L);
cmd.setCommandNo("CMD-20260614150000-0001");
cmd.setCommandTitle("测试指令");
cmd.setCommandContent("测试内容");
cmd.setCommandType("normal");
cmd.setStatus(status);
cmd.setTargetIds("[1,2]");
return cmd;
}
private DispatchExecution buildExecution(String status) {
DispatchExecution exec = new DispatchExecution();
exec.setId(1L);
exec.setCommandId(1L);
exec.setUserId(1L);
exec.setUserName("张三");
exec.setExecuteStatus(status);
return exec;
}
}
@@ -0,0 +1,97 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.production.entity.DispatchTracking;
import com.water.production.mapper.DispatchTrackingMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DispatchTrackingServiceTest {
@Mock
private DispatchTrackingMapper trackingMapper;
@InjectMocks
private DispatchTrackingService trackingService;
@Test
void testLog() {
when(trackingMapper.insert(any())).thenReturn(1);
trackingService.log(1L, null, "create", null, null, "draft", "创建指令");
ArgumentCaptor<DispatchTracking> captor = ArgumentCaptor.forClass(DispatchTracking.class);
verify(trackingMapper).insert(captor.capture());
DispatchTracking saved = captor.getValue();
assertEquals(1L, saved.getCommandId());
assertNull(saved.getExecutionId());
assertEquals("create", saved.getAction());
assertEquals("draft", saved.getToStatus());
assertEquals("创建指令", saved.getRemark());
}
@Test
void testLogWithExecution() {
when(trackingMapper.insert(any())).thenReturn(1);
trackingService.log(1L, 5L, "receive", 10L, "张三", "pending", "received", "接收确认");
ArgumentCaptor<DispatchTracking> captor = ArgumentCaptor.forClass(DispatchTracking.class);
verify(trackingMapper).insert(captor.capture());
DispatchTracking saved = captor.getValue();
assertEquals(5L, saved.getExecutionId());
assertEquals(10L, saved.getOperatorId());
assertEquals("张三", saved.getOperatorName());
assertEquals("pending", saved.getFromStatus());
assertEquals("received", saved.getToStatus());
}
@Test
void testGetTrackingLogs() {
DispatchTracking t1 = new DispatchTracking();
t1.setId(1L);
t1.setCommandId(1L);
t1.setAction("create");
DispatchTracking t2 = new DispatchTracking();
t2.setId(2L);
t2.setCommandId(1L);
t2.setAction("issue");
when(trackingMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(t1, t2));
List<DispatchTracking> logs = trackingService.getTrackingLogs(1L);
assertEquals(2, logs.size());
assertEquals("create", logs.get(0).getAction());
assertEquals("issue", logs.get(1).getAction());
}
@Test
void testGetExecutionTrackingLogs() {
DispatchTracking t1 = new DispatchTracking();
t1.setId(3L);
t1.setExecutionId(5L);
t1.setAction("receive");
when(trackingMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(t1));
List<DispatchTracking> logs = trackingService.getExecutionTrackingLogs(5L);
assertEquals(1, logs.size());
assertEquals(5L, logs.get(0).getExecutionId());
}
}