搭建消息告警模块以及跨网推送服务,支持企微,飞书,钉钉通知

This commit is contained in:
2026-09-03 14:02:35 +08:00
parent cfa8e78d33
commit 3927b0c201
40 changed files with 4360 additions and 7 deletions
@@ -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>
/// 趋势统计(granularityday/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>
/// 分布统计(dimensiondevice/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
}
}