using Admin.NET.Plugin.AiDOP.Dto.S8; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Infrastructure.S8; namespace Admin.NET.Plugin.AiDOP.Service.S8; public class S8MonitoringService : ITransient { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _typeRep; private readonly SqlSugarRepository _orgRep; public S8MonitoringService( SqlSugarRepository rep, SqlSugarRepository typeRep, SqlSugarRepository orgRep) { _rep = rep; _typeRep = typeRep; _orgRep = orgRep; } /// /// 9宫格数据:S1-S7 订单健康分布 + S8业务类别汇总 + S9部门汇总。 /// 数据来源 ado_s8_exception 聚合;异常表为空时返回全 0(不再返回 Demo 数据)。 /// public async Task GetOrderGridAsync(long tenantId = 1, long factoryId = 1) { var events = await _rep.AsQueryable() .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted) .Select(e => new { e.ModuleCode, e.Severity, e.Status, e.TimeoutFlag, e.ExceptionTypeCode, e.SceneCode, e.ResponsibleDeptId, e.CreatedAt, e.ClosedAt }) .ToListAsync(); // ── Modules:按 module_code 聚合 ── var modules = S8ModuleCode.All.Select(mc => { var rows = events.Where(e => e.ModuleCode == mc).ToList(); var unclosed = rows.Where(e => e.Status != "CLOSED").ToList(); var red = unclosed.Count(e => e.Severity == "CRITICAL" || e.Severity == "HIGH"); var yellow = unclosed.Count(e => e.Severity == "MEDIUM"); var green = unclosed.Count(e => e.Severity == "LOW"); var closed = rows.Count(e => e.Status == "CLOSED"); var total = rows.Count; return new AdoS8ModuleOrderSummary { ModuleCode = mc, ModuleLabel = S8ModuleCode.Label(mc), Green = green, Yellow = yellow, Red = red, Total = total, Frequency = total, AvgProcessHours = AvgHours(rows.Select(r => (r.CreatedAt, r.ClosedAt))), CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1), }; }).ToList(); // ── ByCategory:按异常类型的 domain_code/type_code 聚合为 5 大业务类别 ── var typeMap = (await _typeRep.AsQueryable() .Where(t => (t.TenantId == 0 && t.FactoryId == 0) || (t.TenantId == tenantId && t.FactoryId == factoryId)) .ToListAsync()) .GroupBy(t => t.TypeCode) .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.FactoryId).First()); var byCategory = events .Where(e => !string.IsNullOrEmpty(e.ExceptionTypeCode) && typeMap.ContainsKey(e.ExceptionTypeCode!)) .GroupBy(e => CategoryOf(typeMap[e.ExceptionTypeCode!])) .Where(g => !string.IsNullOrEmpty(g.Key)) .Select(g => { var total = g.Count(); var closed = g.Count(e => e.Status == "CLOSED"); return new AdoS8CategorySummary { Category = g.Key!, Total = total, AvgProcessHours = AvgHours(g.Select(e => (e.CreatedAt, e.ClosedAt))), CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1), }; }) .OrderBy(c => CategoryOrder(c.Category)) .ToList(); // ── ByDept:按 ResponsibleDeptId 聚合,JOIN SysOrg 取部门名称 ── var deptIds = events.Select(e => e.ResponsibleDeptId).Where(id => id > 0).Distinct().ToList(); var deptNameMap = deptIds.Count == 0 ? new Dictionary() : (await _orgRep.AsQueryable().Where(o => deptIds.Contains(o.Id)).Select(o => new { o.Id, o.Name }).ToListAsync()) .ToDictionary(o => o.Id, o => o.Name); var byDept = events .Where(e => e.ResponsibleDeptId > 0) .GroupBy(e => e.ResponsibleDeptId) .Select(g => { var total = g.Count(); var closed = g.Count(e => e.Status == "CLOSED"); return new AdoS8DeptSummary { DeptName = deptNameMap.TryGetValue(g.Key, out var n) ? n : $"部门{g.Key}", Total = total, AvgProcessHours = AvgHours(g.Select(e => (e.CreatedAt, e.ClosedAt))), CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1), }; }) .OrderByDescending(d => d.Total) .ToList(); return new AdoS8OrderGridDto { Modules = modules, ByCategory = byCategory, ByDept = byDept, }; } private static double AvgHours(IEnumerable<(DateTime createdAt, DateTime? closedAt)> items) { var closed = items.Where(x => x.closedAt.HasValue).ToList(); if (closed.Count == 0) return 0; var avg = closed.Average(x => (x.closedAt!.Value - x.createdAt).TotalHours); return Math.Round(avg, 1); } /// 异常类型 → 业务类别(与 Overview 页 5 张类别卡对应)。 private static string CategoryOf(AdoS8ExceptionType t) => t.TypeCode switch { "ORDER_CHANGE" => "订单评审", "DELIVERY_DELAY" => "总装发货", "PENDING_SHIPMENT" => "总装发货", "EQUIP_FAULT" => "本体生产", "MATERIAL_SHORTAGE" => "本体生产", "QUALITY_DEFECT" => "本体生产", "SUPPLIER_ETA_ISSUE" => "材料采购", "SUPPLIER_SHIP_ISSUE" => "材料采购", "WH_INBOUND_ISSUE" => "材料采购", "IQC_ISSUE" => "材料采购", "WH_PUTAWAY_ISSUE" => "材料采购", "WH_KIT_ISSUE" => "本体生产", "WH_ISSUE_OUT_ISSUE" => "本体生产", _ => string.Empty, }; private static int CategoryOrder(string category) => category switch { "订单评审" => 1, "产品设计" => 2, "材料采购" => 3, "本体生产" => 4, "总装发货" => 5, _ => 99, }; /// /// 异常监控汇总:按 module_code 分组统计红/黄/绿/超时数。 /// 供综合全景页顶部徽标和模块汇总表使用。 /// public async Task GetSummaryAsync(AdoS8MonitoringSummaryQueryDto q) { var query = _rep.AsQueryable() .Where(e => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), e => e.SceneCode == q.SceneCode) .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), e => e.ModuleCode == q.ModuleCode) .WhereIF(q.BizDateFrom.HasValue, e => e.CreatedAt >= q.BizDateFrom!.Value) .WhereIF(q.BizDateTo.HasValue, e => e.CreatedAt <= q.BizDateTo!.Value); // 聚合到内存(数据量在可控范围内,避免复杂 GROUP BY 兼容性问题) var raw = await query .Select(e => new { e.ModuleCode, e.SceneCode, e.Severity, e.TimeoutFlag, e.Status }) .ToListAsync(); var byModule = raw .GroupBy(e => new { mc = e.ModuleCode ?? string.Empty, sc = e.SceneCode ?? string.Empty }) .Select(g => new AdoS8ModuleSummaryItem { ModuleCode = g.Key.mc, ModuleLabel = S8ModuleCode.Label(g.Key.mc), SceneCode = g.Key.sc, SceneLabel = S8SceneCode.Label(g.Key.sc), Total = g.Count(), Red = g.Count(e => e.Severity == "CRITICAL" || e.Severity == "HIGH"), Yellow = g.Count(e => e.Severity == "MEDIUM"), Green = g.Count(e => e.Severity == "LOW"), Timeout = g.Count(e => e.TimeoutFlag && e.Status != "CLOSED") }) // 按 S8ModuleCode.All 顺序排列 .OrderBy(r => Array.IndexOf(S8ModuleCode.All, r.ModuleCode)) .ToList(); return new AdoS8MonitoringSummaryDto { Total = raw.Count, Red = raw.Count(e => e.Severity == "CRITICAL" || e.Severity == "HIGH"), Yellow = raw.Count(e => e.Severity == "MEDIUM"), Green = raw.Count(e => e.Severity == "LOW"), Timeout = raw.Count(e => e.TimeoutFlag && e.Status != "CLOSED"), ByModule = byModule }; } }