Files

413 lines
19 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
/// 趋势统计(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
}
}