搭建消息告警模块以及跨网推送服务,支持企微,飞书,钉钉通知
This commit is contained in:
@@ -1,12 +1,412 @@
|
||||
using Model;
|
||||
using Model.Dto.Inspection;
|
||||
using Model.Entity.Inspection;
|
||||
using Model.Mapper;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息告警 服务实现
|
||||
/// 消息告警 服务实现(告警列表/闭环操作/统计分析)
|
||||
/// </summary>
|
||||
public class AlertMessageService : IAlertMessageService
|
||||
{
|
||||
// TODO: 实现 消息告警 相关方法
|
||||
#region 查询
|
||||
/// <summary>
|
||||
/// 分页查询告警(支持级别/状态/类型/来源/设备/关键字/时间范围筛选)
|
||||
/// </summary>
|
||||
public async Task<Result<List<AlertMessageDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total,
|
||||
AlertLevelEnum? level, AlertStatusEnum? status, AlertTypeEnum? type, AlertSourceEnum? source,
|
||||
string? deviceCode, string? keyword, DateTime? startDate, DateTime? endDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(level.HasValue, x => x.AlertLevel == level!.Value)
|
||||
.WhereIF(status.HasValue, x => x.AlertStatus == status!.Value)
|
||||
.WhereIF(type.HasValue, x => x.AlertType == type!.Value)
|
||||
.WhereIF(source.HasValue, x => x.Source == source!.Value)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(deviceCode), x => x.DeviceCode == deviceCode)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
|
||||
x => x.DeviceCode!.Contains(keyword!) || x.DeviceName!.Contains(keyword!) || x.Content!.Contains(keyword!))
|
||||
.WhereIF(startDate.HasValue, x => x.FirstAlertTime >= startDate!.Value)
|
||||
.WhereIF(endDate.HasValue, x => x.FirstAlertTime <= endDate!.Value)
|
||||
.OrderBy(x => x.LastAlertTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
// total 保持为总记录数(前端分页组件需要)
|
||||
return Result<List<AlertMessageDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<AlertMessageDto>>.Error("分页查询告警失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 告警详情
|
||||
/// </summary>
|
||||
public async Task<Result<AlertMessageDto>> GetByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0)
|
||||
.FirstAsync();
|
||||
if (entity == null)
|
||||
{
|
||||
return Result<AlertMessageDto>.Error("告警不存在或已被删除");
|
||||
}
|
||||
return Result<AlertMessageDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<AlertMessageDto>.Error("查询告警详情失败", ex);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 闭环操作
|
||||
/// <summary>
|
||||
/// 确认告警
|
||||
/// </summary>
|
||||
public Task<Result<bool>> AcknowledgeAsync(long id, AlertOperateDto dto)
|
||||
{
|
||||
return OperateAsync(id, dto, AlertStatusEnum.Acknowledged, setAck: true, setHandle: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理告警(未确认时同时补记确认信息,处理即视为已确认)
|
||||
/// </summary>
|
||||
public Task<Result<bool>> HandleAsync(long id, AlertOperateDto dto)
|
||||
{
|
||||
return OperateAsync(id, dto, AlertStatusEnum.Handled, setAck: true, setHandle: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭告警(处理完成后归档)
|
||||
/// </summary>
|
||||
public Task<Result<bool>> CloseAsync(long id, AlertOperateDto dto)
|
||||
{
|
||||
return OperateAsync(id, dto, AlertStatusEnum.Closed, setAck: false, setHandle: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 忽略告警(误报等场景,记录忽略原因)
|
||||
/// </summary>
|
||||
public Task<Result<bool>> IgnoreAsync(long id, AlertOperateDto dto)
|
||||
{
|
||||
return OperateAsync(id, dto, AlertStatusEnum.Ignored, setAck: false, setHandle: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转工单(一期仅变更状态并记录 WorkOrderId,工单模块二期对接)
|
||||
/// </summary>
|
||||
public async Task<Result<bool>> ToWorkOrderAsync(long id, AlertOperateDto dto)
|
||||
{
|
||||
var result = await OperateAsync(id, dto, AlertStatusEnum.ToWorkOrder, setAck: false, setHandle: false);
|
||||
if (result.IsSuccess && dto?.WorkOrderId > 0)
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<AlertMessageEntity>()
|
||||
.SetColumns(x => x.WorkOrderId == dto.WorkOrderId)
|
||||
.Where(x => x.Id == id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 闭环操作统一实现:校验终态 → 更新状态与操作人/时间
|
||||
/// </summary>
|
||||
private async Task<Result<bool>> OperateAsync(long id, AlertOperateDto dto, AlertStatusEnum target, bool setAck, bool setHandle)
|
||||
{
|
||||
if (id <= 0)
|
||||
{
|
||||
return Result<bool>.Error("告警 Id 无效");
|
||||
}
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0)
|
||||
.FirstAsync();
|
||||
if (entity == null)
|
||||
{
|
||||
return Result<bool>.Error("告警不存在或已被删除");
|
||||
}
|
||||
// 已关闭/已忽略为终态,不允许再流转
|
||||
if (entity.AlertStatus is AlertStatusEnum.Closed or AlertStatusEnum.Ignored)
|
||||
{
|
||||
return Result<bool>.Error($"告警已处于【{StatusLabel(entity.AlertStatus)}】终态,无法再变更");
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
string? op = dto?.Operator;
|
||||
string? remark = dto?.Remark;
|
||||
|
||||
var updater = SqlSugarContext.DbContext.Updateable<AlertMessageEntity>()
|
||||
.SetColumns(x => x.AlertStatus == target);
|
||||
if (setAck && entity.AckTime == null)
|
||||
{
|
||||
updater = updater.SetColumns(x => new AlertMessageEntity { AckBy = op, AckTime = now });
|
||||
}
|
||||
if (setHandle)
|
||||
{
|
||||
updater = updater.SetColumns(x => new AlertMessageEntity { HandleBy = op, HandleTime = now, HandleRemark = remark });
|
||||
}
|
||||
var rows = await updater.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
return Result<bool>.Success(rows > 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Error("告警操作失败", ex);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 统计分析
|
||||
/// <summary>
|
||||
/// 告警概览(列表页顶部卡片)
|
||||
/// </summary>
|
||||
public async Task<Result<AlertOverviewDto>> GetOverviewAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var db = SqlSugarContext.DbContext;
|
||||
var overview = new AlertOverviewDto
|
||||
{
|
||||
UnacknowledgedCount = await db.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.AlertStatus == AlertStatusEnum.Unacknowledged).CountAsync(),
|
||||
TodayCount = await db.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.FirstAlertTime >= today).CountAsync(),
|
||||
TodayUrgentCount = await db.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.FirstAlertTime >= today && x.AlertLevel == AlertLevelEnum.Urgent).CountAsync(),
|
||||
WeekCount = await db.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.FirstAlertTime >= today.AddDays(-6)).CountAsync()
|
||||
};
|
||||
return Result<AlertOverviewDto>.Success(overview);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<AlertOverviewDto>.Error("查询告警概览失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 趋势统计(granularity:day/week/month;缺省时间范围为近30天)
|
||||
/// </summary>
|
||||
public async Task<Result<List<AlertTrendItemDto>>> GetTrendAsync(string granularity, DateTime? startDate, DateTime? endDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (start, end) = NormalizeRange(startDate, endDate, 30);
|
||||
// 告警经合并窗口去重后数据量可控,取必要列内存聚合(跨库兼容,避免各数据库日期函数差异)
|
||||
var rows = await SqlSugarContext.DbContext.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.FirstAlertTime >= start && x.FirstAlertTime <= end)
|
||||
.Select(x => new { x.FirstAlertTime, x.AlertLevel, x.AlertStatus })
|
||||
.ToListAsync();
|
||||
|
||||
var grouped = rows.GroupBy(x => BucketOf(x.FirstAlertTime, granularity))
|
||||
.OrderBy(g => g.Key, StringComparer.Ordinal)
|
||||
.Select(g => new AlertTrendItemDto
|
||||
{
|
||||
Bucket = g.Key,
|
||||
Total = g.Count(),
|
||||
InfoCount = g.Count(x => x.AlertLevel == AlertLevelEnum.Info),
|
||||
WarningCount = g.Count(x => x.AlertLevel == AlertLevelEnum.Warning),
|
||||
UrgentCount = g.Count(x => x.AlertLevel == AlertLevelEnum.Urgent),
|
||||
UnhandledCount = g.Count(x => x.AlertStatus is AlertStatusEnum.Unacknowledged or AlertStatusEnum.Acknowledged)
|
||||
}).ToList();
|
||||
return Result<List<AlertTrendItemDto>>.Success(grouped);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<AlertTrendItemDto>>.Error("查询告警趋势失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分布统计(dimension:device/type/level/source)
|
||||
/// </summary>
|
||||
public async Task<Result<List<AlertDistributionItemDto>>> GetDistributionAsync(string dimension, DateTime? startDate, DateTime? endDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (start, end) = NormalizeRange(startDate, endDate, 30);
|
||||
var rows = await SqlSugarContext.DbContext.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.FirstAlertTime >= start && x.FirstAlertTime <= end)
|
||||
.Select(x => new { x.DeviceCode, x.DeviceName, x.AlertType, x.AlertLevel, x.Source })
|
||||
.ToListAsync();
|
||||
|
||||
List<AlertDistributionItemDto> result = (dimension ?? "level").ToLower() switch
|
||||
{
|
||||
"device" => rows.GroupBy(x => string.IsNullOrWhiteSpace(x.DeviceName) ? x.DeviceCode ?? "未知设备" : $"{x.DeviceName}({x.DeviceCode})")
|
||||
.Select(g => new AlertDistributionItemDto { Name = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count).Take(20).ToList(),
|
||||
"type" => rows.GroupBy(x => TypeLabel(x.AlertType))
|
||||
.Select(g => new AlertDistributionItemDto { Name = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count).ToList(),
|
||||
"source" => rows.GroupBy(x => SourceLabel(x.Source))
|
||||
.Select(g => new AlertDistributionItemDto { Name = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count).ToList(),
|
||||
_ => rows.GroupBy(x => AlertLevelMap.ToLabel(x.AlertLevel))
|
||||
.Select(g => new AlertDistributionItemDto { Name = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count).ToList()
|
||||
};
|
||||
return Result<List<AlertDistributionItemDto>>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<AlertDistributionItemDto>>.Error("查询告警分布失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高频告警 TOP 排名(按设备聚合,次数含合并累计 TriggerCount)
|
||||
/// </summary>
|
||||
public async Task<Result<List<AlertTopItemDto>>> GetTopAsync(int topN, DateTime? startDate, DateTime? endDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (topN <= 0 || topN > 100) topN = 10;
|
||||
var (start, end) = NormalizeRange(startDate, endDate, 30);
|
||||
var rows = await SqlSugarContext.DbContext.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.FirstAlertTime >= start && x.FirstAlertTime <= end)
|
||||
.Select(x => new { x.DeviceCode, x.DeviceName, x.TriggerCount, x.LastAlertTime })
|
||||
.ToListAsync();
|
||||
|
||||
var top = rows.GroupBy(x => x.DeviceCode ?? "未知设备")
|
||||
.Select(g => new AlertTopItemDto
|
||||
{
|
||||
DeviceCode = g.Key,
|
||||
DeviceName = g.First().DeviceName,
|
||||
Count = g.Sum(x => x.TriggerCount),
|
||||
LastAlertTime = g.Max(x => x.LastAlertTime)
|
||||
})
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(topN)
|
||||
.ToList();
|
||||
return Result<List<AlertTopItemDto>>.Success(top);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<AlertTopItemDto>>.Error("查询高频告警失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理及时率统计(平均确认/处理时长;及时率 = 30分钟内确认的占比)
|
||||
/// </summary>
|
||||
public async Task<Result<AlertTimelinessDto>> GetTimelinessAsync(DateTime? startDate, DateTime? endDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (start, end) = NormalizeRange(startDate, endDate, 30);
|
||||
var rows = await SqlSugarContext.DbContext.Queryable<AlertMessageEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.FirstAlertTime >= start && x.FirstAlertTime <= end)
|
||||
.Select(x => new { x.FirstAlertTime, x.AckTime, x.HandleTime, x.AlertStatus })
|
||||
.ToListAsync();
|
||||
|
||||
const int timelyMinutes = 30;
|
||||
var acked = rows.Where(x => x.AckTime != null).ToList();
|
||||
var handled = rows.Where(x => x.HandleTime != null &&
|
||||
x.AlertStatus is AlertStatusEnum.Handled or AlertStatusEnum.Closed).ToList();
|
||||
|
||||
var result = new AlertTimelinessDto
|
||||
{
|
||||
Total = rows.Count,
|
||||
AckCount = acked.Count,
|
||||
HandledCount = handled.Count,
|
||||
UnhandledCount = rows.Count(x => x.AlertStatus is AlertStatusEnum.Unacknowledged or AlertStatusEnum.Acknowledged),
|
||||
AvgAckMinutes = acked.Count > 0
|
||||
? Math.Round(acked.Average(x => (x.AckTime!.Value - x.FirstAlertTime).TotalMinutes), 1)
|
||||
: null,
|
||||
AvgHandleMinutes = handled.Count > 0
|
||||
? Math.Round(handled.Average(x => (x.HandleTime!.Value - x.FirstAlertTime).TotalMinutes), 1)
|
||||
: null,
|
||||
TimelyRate = rows.Count > 0
|
||||
? Math.Round(100.0 * acked.Count(x => (x.AckTime!.Value - x.FirstAlertTime).TotalMinutes <= timelyMinutes) / rows.Count, 1)
|
||||
: 0
|
||||
};
|
||||
return Result<AlertTimelinessDto>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<AlertTimelinessDto>.Error("查询及时率统计失败", ex);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 私有工具
|
||||
/// <summary>
|
||||
/// 时间范围缺省处理:未传时默认近 defaultDays 天
|
||||
/// </summary>
|
||||
private static (DateTime start, DateTime end) NormalizeRange(DateTime? startDate, DateTime? endDate, int defaultDays)
|
||||
{
|
||||
var end = endDate ?? DateTime.Now;
|
||||
var start = startDate ?? end.AddDays(-defaultDays);
|
||||
return (start, end);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 时间分桶(day:yyyy-MM-dd / week:yyyy-Www / month:yyyy-MM)
|
||||
/// </summary>
|
||||
private static string BucketOf(DateTime time, string granularity)
|
||||
{
|
||||
return (granularity ?? "day").ToLower() switch
|
||||
{
|
||||
"week" => $"{ISOWeek.GetYear(time)}-W{ISOWeek.GetWeekOfYear(time):D2}",
|
||||
"month" => time.ToString("yyyy-MM"),
|
||||
_ => time.ToString("yyyy-MM-dd")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 告警状态中文标签
|
||||
/// </summary>
|
||||
public static string StatusLabel(AlertStatusEnum status) => status switch
|
||||
{
|
||||
AlertStatusEnum.Unacknowledged => "未确认",
|
||||
AlertStatusEnum.Acknowledged => "已确认",
|
||||
AlertStatusEnum.Handled => "已处理",
|
||||
AlertStatusEnum.Closed => "已关闭",
|
||||
AlertStatusEnum.Ignored => "已忽略",
|
||||
AlertStatusEnum.ToWorkOrder => "已转工单",
|
||||
_ => "未知"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 告警类型中文标签
|
||||
/// </summary>
|
||||
private static string TypeLabel(AlertTypeEnum type) => type switch
|
||||
{
|
||||
AlertTypeEnum.ThresholdExceed => "阈值超限",
|
||||
AlertTypeEnum.DeviceOffline => "设备离线",
|
||||
AlertTypeEnum.CommFault => "通讯故障",
|
||||
AlertTypeEnum.PatrolAbnormal => "巡检异常",
|
||||
AlertTypeEnum.Custom => "自定义",
|
||||
_ => "其它"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 告警来源中文标签
|
||||
/// </summary>
|
||||
private static string SourceLabel(AlertSourceEnum source) => source switch
|
||||
{
|
||||
AlertSourceEnum.RuleEngine => "规则引擎",
|
||||
AlertSourceEnum.AutoPatrol => "自动巡检",
|
||||
AlertSourceEnum.Dashboard => "数据看板",
|
||||
AlertSourceEnum.Collection => "数据采集",
|
||||
AlertSourceEnum.Manual => "手动上报",
|
||||
_ => "其它"
|
||||
};
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user