using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing; using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse; using Admin.NET.Plugin.AiDOP.Entity.S8; namespace Admin.NET.Plugin.AiDOP.Service.S8; public class S8DashboardService : ITransient { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _deptRep; private readonly SqlSugarRepository _processNodeRep; public S8DashboardService( SqlSugarRepository rep, SqlSugarRepository deptRep, SqlSugarRepository processNodeRep) { _rep = rep; _deptRep = deptRep; _processNodeRep = processNodeRep; } public async Task GetOverviewAsync(long tenantId, long factoryId, DateTime? beginTime = null, DateTime? endTime = null, string? severity = null) { // 看板顶部的"开始/结束日期"通过 beginTime/endTime 透传,与列表的 beginTime/endTime 同语义。 // BUG-12:补 severity 过滤,与异常列表 API 的 severity 口径一致(精确匹配 LOW/MEDIUM/HIGH/CRITICAL)。 // BUG-22:REJECTED 状态归入 pending 桶,避免从总数视角"消失";与列表 statusBucket="pending" 同步对齐。 var q = _rep.AsQueryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted) .WhereIF(beginTime.HasValue, x => x.CreatedAt >= beginTime!.Value) .WhereIF(endTime.HasValue, x => x.CreatedAt <= endTime!.Value) .WhereIF(!string.IsNullOrWhiteSpace(severity), x => x.Severity == severity); var total = await q.CountAsync(); var pending = await q.CountAsync(x => x.Status == "NEW" || x.Status == "ASSIGNED" || x.Status == "IN_PROGRESS" || x.Status == "PENDING_VERIFICATION" || x.Status == "REJECTED"); var inProgress = await q.CountAsync(x => x.Status == "IN_PROGRESS"); var timeout = await q.CountAsync(x => x.TimeoutFlag); var closed = await q.CountAsync(x => x.Status == "CLOSED"); var todayNew = await q.CountAsync(x => x.CreatedAt >= DateTime.Today); var critical = await q.CountAsync(x => x.Severity == "CRITICAL"); var closureRate = total > 0 ? Math.Round(closed * 100.0 / total, 1) : 0.0; // 平均处理周期(小时),仅对已闭环且有关闭时间的记录计算 var closedRows = await q .Where(x => x.Status == "CLOSED" && x.ClosedAt != null) .Select(x => new { x.CreatedAt, x.ClosedAt }) .ToListAsync(); var avgCycleHours = closedRows.Count > 0 ? Math.Round(closedRows.Average(x => (x.ClosedAt!.Value - x.CreatedAt).TotalHours), 1) : 0.0; return new { total, pending, inProgress, timeout, closed, todayNew, critical, closureRate, avgCycleHours }; } public async Task GetTrendsAsync(long tenantId, long factoryId, int days) { var from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90)); var rows = await _rep.AsQueryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from) .Select(x => new { x.CreatedAt }) .ToListAsync(); return rows .GroupBy(x => x.CreatedAt.Date) .OrderBy(g => g.Key) .Select(g => new { date = g.Key.ToString("yyyy-MM-dd"), count = g.Count() }) .ToList(); } public async Task GetDistributionsAsync(long tenantId, long factoryId) { var list = await _rep.AsQueryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted) .Select(x => new { x.Status, x.SceneCode, x.Severity, x.ResponsibleDeptId, x.OccurrenceDeptId, x.ProcessNodeCode, x.RelatedObjectCode, }) .ToListAsync(); // 部门名称字典 var allDeptIds = list.Select(x => x.ResponsibleDeptId) .Concat(list.Select(x => x.OccurrenceDeptId)) .Distinct().ToList(); var deptMap = await _deptRep.AsQueryable() .Where(d => allDeptIds.Contains(d.Id)) .Select(d => new { d.Id, Name = d.Descr ?? d.Department }) .ToListAsync(); var deptDict = deptMap.ToDictionary(d => d.Id, d => d.Name ?? d.Id.ToString()); // 流程节点名称字典 var processNodes = await _processNodeRep.AsQueryable() .OrderBy(p => p.SortNo) .Select(p => new { p.Code, p.Name }) .ToListAsync(); var processDict = processNodes.ToDictionary(p => p.Code, p => p.Name); return new { byStatus = list .GroupBy(x => x.Status) .Select(g => new { key = g.Key, count = g.Count() }), byScene = list .GroupBy(x => x.SceneCode) .Select(g => new { key = g.Key, count = g.Count() }), bySeverity = list .GroupBy(x => x.Severity) .Select(g => new { key = g.Key, count = g.Count() }), byDept = list .GroupBy(x => x.ResponsibleDeptId) .Select(g => new { key = g.Key, deptName = deptDict.GetValueOrDefault(g.Key, g.Key.ToString()), count = g.Count(), }), byOccurrenceDept = list .GroupBy(x => x.OccurrenceDeptId) .Select(g => new { key = g.Key, deptName = deptDict.GetValueOrDefault(g.Key, g.Key.ToString()), count = g.Count(), }), byProcess = list .Where(x => x.ProcessNodeCode != null) .GroupBy(x => x.ProcessNodeCode!) .Select(g => new { key = g.Key, nodeName = processDict.GetValueOrDefault(g.Key, g.Key), count = g.Count(), }), byObject = list .Where(x => x.RelatedObjectCode != null) .GroupBy(x => x.RelatedObjectCode!) .OrderByDescending(g => g.Count()) .Take(20) .Select(g => new { key = g.Key, count = g.Count() }), }; } public async Task GetDeptBacklogAsync(long tenantId, long factoryId) { var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS" }; var list = await _rep.AsQueryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted) .Select(x => new { x.ResponsibleDeptId, x.Status, x.TimeoutFlag }) .ToListAsync(); var deptIds = list.Select(x => x.ResponsibleDeptId).Distinct().ToList(); var deptMap = await _deptRep.AsQueryable() .Where(d => deptIds.Contains(d.Id)) .Select(d => new { d.Id, Name = d.Descr ?? d.Department }) .ToListAsync(); var deptDict = deptMap.ToDictionary(d => d.Id, d => d.Name ?? d.Id.ToString()); return list .GroupBy(x => x.ResponsibleDeptId) .Select(g => new { deptId = g.Key, deptName = deptDict.GetValueOrDefault(g.Key, g.Key.ToString()), pending = g.Count(x => pendingStatuses.Contains(x.Status)), inProgress = g.Count(x => x.Status == "IN_PROGRESS"), timeout = g.Count(x => x.TimeoutFlag), total = g.Count(), }) .OrderByDescending(x => x.pending) .ToList(); } /// /// 按维度返回多系列日趋势数据。dim: object | process | occDept | respDept /// 返回: { dates, series: [{ name, data[] }] } /// public async Task GetDimTrendsAsync(long tenantId, long factoryId, string dim, int days) { var from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90)); var rows = await _rep.AsQueryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from) .Select(x => new { x.CreatedAt, x.ProcessNodeCode, x.RelatedObjectCode, x.OccurrenceDeptId, x.ResponsibleDeptId, }) .ToListAsync(); // 生成连续日期序列 var dates = Enumerable.Range(0, (DateTime.Today - from).Days + 1) .Select(i => from.AddDays(i).Date) .ToList(); var dateLabels = dates.Select(d => d.ToString("yyyy-MM-dd")).ToList(); // 按维度取 key 函数 Func keySelector = dim switch { "process" => r => r.ProcessNodeCode ?? "未设置", "occDept" => r => r.OccurrenceDeptId.ToString(), "respDept" => r => r.ResponsibleDeptId.ToString(), _ => r => r.RelatedObjectCode ?? "未设置", // object }; var grouped = rows .GroupBy(r => keySelector(r)) .OrderByDescending(g => g.Count()) .Take(8) // 最多取 8 个系列,避免图例过多 .ToList(); // 补全部门名 / 流程节点名 Dictionary nameMap = new(); if (dim == "process") { var nodes = await _processNodeRep.AsQueryable().ToListAsync(); nameMap = nodes.ToDictionary(n => n.Code, n => n.Name); } else if (dim is "occDept" or "respDept") { var ids = grouped.Select(g => long.TryParse(g.Key, out var id) ? id : 0).ToList(); var depts = await _deptRep.AsQueryable() .Where(d => ids.Contains(d.Id)) .Select(d => new { d.Id, Name = d.Descr ?? d.Department }) .ToListAsync(); nameMap = depts.ToDictionary(d => d.Id.ToString(), d => d.Name ?? d.Id.ToString()); } var series = grouped.Select(g => { var seriesName = nameMap.TryGetValue(g.Key, out var n) ? n : g.Key; var byDate = g.GroupBy(r => r.CreatedAt.Date).ToDictionary(x => x.Key, x => x.Count()); var data = dates.Select(d => byDate.TryGetValue(d, out var c) ? c : 0).ToList(); return new { name = seriesName, data }; }).ToList(); return new { dates = dateLabels, series }; } public async Task> GetQuickExceptionsAsync(long tenantId, long factoryId, string mode) { var q = _rep.AsQueryable().Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted); var ordered = mode switch { "high-priority" => q.OrderBy(x => x.PriorityScore, OrderByType.Desc), "timeout" => q.Where(x => x.TimeoutFlag).OrderBy(x => x.SlaDeadline), _ => q.OrderBy(x => x.CreatedAt, OrderByType.Desc), }; return await ordered.Take(20).ToListAsync(); } }