413 lines
19 KiB
C#
413 lines
19 KiB
C#
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
|
||
{
|
||
#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
|
||
}
|
||
}
|