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 { /// /// 消息告警 服务实现(告警列表/闭环操作/统计分析) /// public class AlertMessageService : IAlertMessageService { #region 查询 /// /// 分页查询告警(支持级别/状态/类型/来源/设备/关键字/时间范围筛选) /// public async Task>> GetPagedAsync(int pageIndex, int pageSize, RefAsync total, AlertLevelEnum? level, AlertStatusEnum? status, AlertTypeEnum? type, AlertSourceEnum? source, string? deviceCode, string? keyword, DateTime? startDate, DateTime? endDate) { try { var list = await SqlSugarContext.DbContext.Queryable() .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>.Success(list.ToDtoList()); } catch (Exception ex) { return Result>.Error("分页查询告警失败", ex); } } /// /// 告警详情 /// public async Task> GetByIdAsync(long id) { try { var entity = await SqlSugarContext.DbContext.Queryable() .Where(x => x.Id == id && x.IsDel == 0) .FirstAsync(); if (entity == null) { return Result.Error("告警不存在或已被删除"); } return Result.Success(entity.ToDto()); } catch (Exception ex) { return Result.Error("查询告警详情失败", ex); } } #endregion #region 闭环操作 /// /// 确认告警 /// public Task> AcknowledgeAsync(long id, AlertOperateDto dto) { return OperateAsync(id, dto, AlertStatusEnum.Acknowledged, setAck: true, setHandle: false); } /// /// 处理告警(未确认时同时补记确认信息,处理即视为已确认) /// public Task> HandleAsync(long id, AlertOperateDto dto) { return OperateAsync(id, dto, AlertStatusEnum.Handled, setAck: true, setHandle: true); } /// /// 关闭告警(处理完成后归档) /// public Task> CloseAsync(long id, AlertOperateDto dto) { return OperateAsync(id, dto, AlertStatusEnum.Closed, setAck: false, setHandle: true); } /// /// 忽略告警(误报等场景,记录忽略原因) /// public Task> IgnoreAsync(long id, AlertOperateDto dto) { return OperateAsync(id, dto, AlertStatusEnum.Ignored, setAck: false, setHandle: true); } /// /// 转工单(一期仅变更状态并记录 WorkOrderId,工单模块二期对接) /// public async Task> 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() .SetColumns(x => x.WorkOrderId == dto.WorkOrderId) .Where(x => x.Id == id) .ExecuteCommandAsync(); } return result; } /// /// 闭环操作统一实现:校验终态 → 更新状态与操作人/时间 /// private async Task> OperateAsync(long id, AlertOperateDto dto, AlertStatusEnum target, bool setAck, bool setHandle) { if (id <= 0) { return Result.Error("告警 Id 无效"); } try { var entity = await SqlSugarContext.DbContext.Queryable() .Where(x => x.Id == id && x.IsDel == 0) .FirstAsync(); if (entity == null) { return Result.Error("告警不存在或已被删除"); } // 已关闭/已忽略为终态,不允许再流转 if (entity.AlertStatus is AlertStatusEnum.Closed or AlertStatusEnum.Ignored) { return Result.Error($"告警已处于【{StatusLabel(entity.AlertStatus)}】终态,无法再变更"); } var now = DateTime.Now; string? op = dto?.Operator; string? remark = dto?.Remark; var updater = SqlSugarContext.DbContext.Updateable() .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.Success(rows > 0); } catch (Exception ex) { return Result.Error("告警操作失败", ex); } } #endregion #region 统计分析 /// /// 告警概览(列表页顶部卡片) /// public async Task> GetOverviewAsync() { try { var today = DateTime.Today; var db = SqlSugarContext.DbContext; var overview = new AlertOverviewDto { UnacknowledgedCount = await db.Queryable() .Where(x => x.IsDel == 0 && x.AlertStatus == AlertStatusEnum.Unacknowledged).CountAsync(), TodayCount = await db.Queryable() .Where(x => x.IsDel == 0 && x.FirstAlertTime >= today).CountAsync(), TodayUrgentCount = await db.Queryable() .Where(x => x.IsDel == 0 && x.FirstAlertTime >= today && x.AlertLevel == AlertLevelEnum.Urgent).CountAsync(), WeekCount = await db.Queryable() .Where(x => x.IsDel == 0 && x.FirstAlertTime >= today.AddDays(-6)).CountAsync() }; return Result.Success(overview); } catch (Exception ex) { return Result.Error("查询告警概览失败", ex); } } /// /// 趋势统计(granularity:day/week/month;缺省时间范围为近30天) /// public async Task>> GetTrendAsync(string granularity, DateTime? startDate, DateTime? endDate) { try { var (start, end) = NormalizeRange(startDate, endDate, 30); // 告警经合并窗口去重后数据量可控,取必要列内存聚合(跨库兼容,避免各数据库日期函数差异) var rows = await SqlSugarContext.DbContext.Queryable() .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>.Success(grouped); } catch (Exception ex) { return Result>.Error("查询告警趋势失败", ex); } } /// /// 分布统计(dimension:device/type/level/source) /// public async Task>> GetDistributionAsync(string dimension, DateTime? startDate, DateTime? endDate) { try { var (start, end) = NormalizeRange(startDate, endDate, 30); var rows = await SqlSugarContext.DbContext.Queryable() .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 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>.Success(result); } catch (Exception ex) { return Result>.Error("查询告警分布失败", ex); } } /// /// 高频告警 TOP 排名(按设备聚合,次数含合并累计 TriggerCount) /// public async Task>> 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() .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>.Success(top); } catch (Exception ex) { return Result>.Error("查询高频告警失败", ex); } } /// /// 处理及时率统计(平均确认/处理时长;及时率 = 30分钟内确认的占比) /// public async Task> GetTimelinessAsync(DateTime? startDate, DateTime? endDate) { try { var (start, end) = NormalizeRange(startDate, endDate, 30); var rows = await SqlSugarContext.DbContext.Queryable() .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.Success(result); } catch (Exception ex) { return Result.Error("查询及时率统计失败", ex); } } #endregion #region 私有工具 /// /// 时间范围缺省处理:未传时默认近 defaultDays 天 /// 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); } /// /// 时间分桶(day:yyyy-MM-dd / week:yyyy-Www / month:yyyy-MM) /// 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") }; } /// /// 告警状态中文标签 /// public static string StatusLabel(AlertStatusEnum status) => status switch { AlertStatusEnum.Unacknowledged => "未确认", AlertStatusEnum.Acknowledged => "已确认", AlertStatusEnum.Handled => "已处理", AlertStatusEnum.Closed => "已关闭", AlertStatusEnum.Ignored => "已忽略", AlertStatusEnum.ToWorkOrder => "已转工单", _ => "未知" }; /// /// 告警类型中文标签 /// private static string TypeLabel(AlertTypeEnum type) => type switch { AlertTypeEnum.ThresholdExceed => "阈值超限", AlertTypeEnum.DeviceOffline => "设备离线", AlertTypeEnum.CommFault => "通讯故障", AlertTypeEnum.PatrolAbnormal => "巡检异常", AlertTypeEnum.Custom => "自定义", _ => "其它" }; /// /// 告警来源中文标签 /// private static string SourceLabel(AlertSourceEnum source) => source switch { AlertSourceEnum.RuleEngine => "规则引擎", AlertSourceEnum.AutoPatrol => "自动巡检", AlertSourceEnum.Dashboard => "数据看板", AlertSourceEnum.Collection => "数据采集", AlertSourceEnum.Manual => "手动上报", _ => "其它" }; #endregion } }