feat(wm-production): #67 报警规则引擎与报警管理中心
- DDL: V2__alert_engine.sql (prod_alert_rule, prod_alert_record, prod_alert_notification, prod_alert_rule_device) - Entity: AlertRule(规则定义), AlertRecord(全生命周期增强), AlertNotification(通知记录) - Mapper: AlertRuleMapper, AlertRecordMapper(自定义关联SQL), AlertNotificationMapper - Service: AlertRuleService(规则CRUD + AND/OR组合条件评估引擎 + 阈值触发 + 多级别报警) - Service: AlertCenterService(报警全生命周期: 确认/派单/处理/归档 + 统计看板) - Controller: AlertController 23个RESTful端点 (/api/production/alert/*) - 单元测试: AlertRuleServiceTest(12个) + AlertCenterServiceTest(5个) = 17个测试
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
-- =============================================
|
||||
-- 智慧水务管理系统 - 报警规则引擎 + 报警管理中心 DDL
|
||||
-- 版本: V2
|
||||
-- =============================================
|
||||
|
||||
-- ==================== 报警规则定义 ====================
|
||||
CREATE TABLE IF NOT EXISTS prod_alert_rule (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
rule_name VARCHAR(100) NOT NULL,
|
||||
rule_code VARCHAR(50) UNIQUE,
|
||||
description TEXT,
|
||||
device_type VARCHAR(30),
|
||||
metric_key VARCHAR(50) NOT NULL,
|
||||
alert_level VARCHAR(10) NOT NULL DEFAULT 'general', -- general/important/urgent
|
||||
condition_expr TEXT NOT NULL, -- JSON: {"op":"AND","conditions":[{"metric":"pressure","operator":">","threshold":0.8},...]}
|
||||
threshold_value DECIMAL(12,4), -- 简单阈值(向后兼容)
|
||||
debounce_sec INT DEFAULT 300,
|
||||
notify_channels VARCHAR(200), -- 逗号分隔: sms,wechat,app,email
|
||||
notify_template VARCHAR(500), -- 通知模板
|
||||
enabled SMALLINT DEFAULT 1,
|
||||
priority INT DEFAULT 0, -- 规则优先级
|
||||
effective_start TIME, -- 生效开始时间
|
||||
effective_end TIME, -- 生效结束时间
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
deleted SMALLINT DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE prod_alert_rule IS '报警规则定义表';
|
||||
COMMENT ON COLUMN prod_alert_rule.alert_level IS '报警等级: general(一般)/important(重要)/urgent(紧急)';
|
||||
COMMENT ON COLUMN prod_alert_rule.condition_expr IS '条件表达式JSON: 支持AND/OR组合条件';
|
||||
|
||||
-- ==================== 报警记录(全生命周期) ====================
|
||||
CREATE TABLE IF NOT EXISTS prod_alert_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
rule_id BIGINT REFERENCES prod_alert_rule(id),
|
||||
rule_name VARCHAR(100),
|
||||
device_id BIGINT,
|
||||
device_sn VARCHAR(100),
|
||||
device_name VARCHAR(200),
|
||||
area VARCHAR(50),
|
||||
metric_key VARCHAR(50) NOT NULL,
|
||||
metric_value DECIMAL(12,4),
|
||||
threshold_value VARCHAR(50),
|
||||
alert_level VARCHAR(10) NOT NULL DEFAULT 'general',
|
||||
title VARCHAR(200),
|
||||
message TEXT,
|
||||
-- 生命周期状态: 0=活跃 1=已确认 2=已派单 3=处理中 4=已处理 5=已归档
|
||||
status INT DEFAULT 0,
|
||||
confirmed_by BIGINT,
|
||||
confirmed_time TIMESTAMP,
|
||||
dispatch_time TIMESTAMP,
|
||||
assignee_id BIGINT,
|
||||
assignee_name VARCHAR(50),
|
||||
handler_id BIGINT,
|
||||
handler_name VARCHAR(50),
|
||||
handle_result TEXT,
|
||||
handle_time TIMESTAMP,
|
||||
archive_time TIMESTAMP,
|
||||
archive_reason VARCHAR(500),
|
||||
resolved_at TIMESTAMP,
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW(),
|
||||
deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
COMMENT ON TABLE prod_alert_record IS '报警记录表(全生命周期)';
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_record_time ON prod_alert_record(created_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_record_device ON prod_alert_record(device_sn, created_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_record_status ON prod_alert_record(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_record_level ON prod_alert_record(alert_level);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_record_area ON prod_alert_record(area);
|
||||
|
||||
-- ==================== 报警通知记录 ====================
|
||||
CREATE TABLE IF NOT EXISTS prod_alert_notification (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
alert_record_id BIGINT REFERENCES prod_alert_record(id),
|
||||
rule_id BIGINT,
|
||||
channel VARCHAR(30) NOT NULL, -- sms/wechat/app/email
|
||||
recipient VARCHAR(100) NOT NULL, -- 接收人标识
|
||||
recipient_name VARCHAR(50),
|
||||
title VARCHAR(200),
|
||||
content TEXT,
|
||||
status INT DEFAULT 0, -- 0=待发送 1=已发送 2=发送失败 3=已读
|
||||
send_time TIMESTAMP,
|
||||
read_time TIMESTAMP,
|
||||
retry_count INT DEFAULT 0,
|
||||
error_msg VARCHAR(500),
|
||||
created_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE prod_alert_notification IS '报警通知记录表';
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_notif_record ON prod_alert_notification(alert_record_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_notif_status ON prod_alert_notification(status);
|
||||
|
||||
-- ==================== 报警规则-设备关联(可选) ====================
|
||||
CREATE TABLE IF NOT EXISTS prod_alert_rule_device (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
rule_id BIGINT REFERENCES prod_alert_rule(id),
|
||||
device_id BIGINT,
|
||||
device_sn VARCHAR(100),
|
||||
area VARCHAR(50),
|
||||
created_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE prod_alert_rule_device IS '报警规则-设备/区域关联表';
|
||||
|
||||
-- ==================== 初始规则数据 ====================
|
||||
INSERT INTO prod_alert_rule (rule_name, rule_code, metric_key, alert_level, condition_expr, threshold_value, debounce_sec, description, enabled) VALUES
|
||||
('管网压力过高报警', 'RULE_PRESSURE_HIGH', 'pressure', 'urgent',
|
||||
'{"op":"AND","conditions":[{"metric":"pressure","operator":">","threshold":0.8}]}',
|
||||
0.8000, 300, '管网压力超过0.8MPa时触发紧急报警', 1),
|
||||
('管网压力过低报警', 'RULE_PRESSURE_LOW', 'pressure', 'important',
|
||||
'{"op":"OR","conditions":[{"metric":"pressure","operator":"<","threshold":0.2}]}',
|
||||
0.2000, 300, '管网压力低于0.2MPa时触发重要报警', 1),
|
||||
('水质浊度超标', 'RULE_TURBIDITY_HIGH', 'turbidity', 'urgent',
|
||||
'{"op":"AND","conditions":[{"metric":"turbidity","operator":">","threshold":1.0}]}',
|
||||
1.0000, 600, '水质浊度超过1.0NTU触发紧急报警', 1),
|
||||
('余氯偏低报警', 'RULE_CHLORINE_LOW', 'residual_chlorine', 'general',
|
||||
'{"op":"AND","conditions":[{"metric":"residual_chlorine","operator":"<","threshold":0.1}]}',
|
||||
0.1000, 600, '余氯低于0.1mg/L触发一般报警', 1),
|
||||
('流量异常波动', 'RULE_FLOW_ANOMALY', 'flow', 'important',
|
||||
'{"op":"OR","conditions":[{"metric":"flow","operator":">","threshold":100},{"metric":"flow","operator":"<","threshold":5}]}',
|
||||
NULL, 120, '流量异常偏高或偏低时触发报警', 1);
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -1,13 +1,106 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data; import java.time.LocalDateTime;
|
||||
@Data @TableName("prod_alert_record")
|
||||
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;
|
||||
private String deviceId, area, level; // 一般/重要/紧急
|
||||
private String alertType, description;
|
||||
private Integer status; // 0活跃 1已确认 2已处理
|
||||
private Long confirmedBy, handledBy;
|
||||
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
|
||||
private LocalDateTime confirmedTime, handledTime;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
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 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,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);
|
||||
}
|
||||
@@ -1,5 +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;
|
||||
@Mapper public interface AlertRecordMapper extends BaseMapper<AlertRecord> {}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -0,0 +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 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,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,366 @@
|
||||
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 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.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();
|
||||
}
|
||||
|
||||
// ==================== 规则评估引擎 ====================
|
||||
|
||||
/**
|
||||
* 评估指标值是否触发报警 (核心引擎)
|
||||
* 支持AND/OR组合条件的JSON表达式解析
|
||||
*
|
||||
* @param deviceSn 设备编号
|
||||
* @param metricKey 指标键
|
||||
* @param value 指标值
|
||||
* @param area 区域
|
||||
* @return 触发的报警记录列表
|
||||
*/
|
||||
@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;
|
||||
|
||||
// 去重检查(debounce)
|
||||
String dedupKey = deviceSn + ":" + metricKey + ":" + rule.getAlertLevel();
|
||||
long now = Instant.now().getEpochSecond();
|
||||
Long lastTime = lastTriggerTime.get(dedupKey);
|
||||
if (lastTime != null && (now - lastTime) < rule.getDebounceSec()) {
|
||||
log.debug("Alert debounced: {} (last={}s ago, debounce={}s)", dedupKey, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并评估条件表达式 (支持AND/OR组合)
|
||||
* 表达式格式:
|
||||
* {"op":"AND","conditions":[
|
||||
* {"metric":"pressure","operator":">","threshold":0.8},
|
||||
* {"metric":"temperature","operator":"<","threshold":50}
|
||||
* ]}
|
||||
*
|
||||
* 或简单格式: {"metric":"pressure","operator":">","threshold":0.8}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估组合条件(AND/OR)
|
||||
*/
|
||||
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;
|
||||
|
||||
// 如果条件指定了不同的metric, 则跳过(当前值不匹配)
|
||||
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 {
|
||||
// 跨天场景: 如 22:00 - 06:00
|
||||
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());
|
||||
// 实际调用: notifyService.send(channel, notification.getTitle(), notification.getContent());
|
||||
}
|
||||
|
||||
private String levelLabel(String level) {
|
||||
return switch (level) {
|
||||
case "urgent" -> "紧急";
|
||||
case "important" -> "重要";
|
||||
case "general" -> "一般";
|
||||
default -> level;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user