using Admin.NET.Plugin.AiDOP.Dto.S8; using Admin.NET.Plugin.AiDOP.Entity.S8; namespace Admin.NET.Plugin.AiDOP.Service.S8; /// /// 大屏卡片数据查询服务:按 (pageCode, cellCode) 读取配置,按 binding_type 分发组装结果。 /// 配置解析:工厂覆盖优先(tenant+factory 精确匹配)→ 全局基线(tenant=0/factory=0)兜底。 /// public class S8DashboardCellDataService : ITransient { private readonly SqlSugarRepository _cfgRep; private readonly SqlSugarRepository _exRep; private readonly SqlSugarRepository _orgRep; public S8DashboardCellDataService( SqlSugarRepository cfgRep, SqlSugarRepository exRep, SqlSugarRepository orgRep) { _cfgRep = cfgRep; _exRep = exRep; _orgRep = orgRep; } public async Task GetAsync(AdoS8CellDataQueryDto q) { if (string.IsNullOrWhiteSpace(q.PageCode) || string.IsNullOrWhiteSpace(q.CellCode)) throw new S8BizException("pageCode 和 cellCode 必填"); var cfg = await ResolveConfigAsync(q.TenantId, q.FactoryId, q.PageCode, q.CellCode); if (cfg is null) return new AdoS8CellDataDto { PageCode = q.PageCode, CellCode = q.CellCode, Message = "未配置", }; if (!cfg.Enabled) return new AdoS8CellDataDto { PageCode = q.PageCode, CellCode = q.CellCode, BindingType = cfg.BindingType, Title = cfg.CellTitle, StatMetric = cfg.StatMetric, TimeWindow = cfg.TimeWindow, Message = "已禁用", }; var (from, to) = TimeRange(cfg.TimeWindow); var effectiveDeptGroupBy = !string.IsNullOrWhiteSpace(q.DeptGroupBy) ? q.DeptGroupBy! : cfg.DeptGroupBy; // 取事实数据 var events = await _exRep.AsQueryable() .Where(e => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted) .Where(e => e.CreatedAt >= from && e.CreatedAt < to) .Select(e => new EvtRow { Status = e.Status, ExceptionTypeCode = e.ExceptionTypeCode, ModuleCode = e.ModuleCode, SceneCode = e.SceneCode, ResponsibleDeptId = e.ResponsibleDeptId, OccurrenceDeptId = e.OccurrenceDeptId, CreatedAt = e.CreatedAt, ClosedAt = e.ClosedAt, }) .ToListAsync(); // 按 binding_type 过滤 IEnumerable scoped = cfg.BindingType switch { "EXCEPTION_TYPE" => events.Where(e => !string.IsNullOrEmpty(cfg.ExceptionTypeCode) && e.ExceptionTypeCode == cfg.ExceptionTypeCode), "AGGREGATE" => ApplyAggregateScope(events, cfg.AggregateScope), _ => events, // CUSTOM:不按类型过滤,返回同页范围聚合值供前端参考 }; var list = scoped.ToList(); var dto = new AdoS8CellDataDto { CellCode = cfg.CellCode, PageCode = cfg.PageCode, Title = cfg.CellTitle, BindingType = cfg.BindingType, StatMetric = cfg.StatMetric, TimeWindow = cfg.TimeWindow, Value = ComputeValue(list, cfg.StatMetric), }; // 明细分组:AGGREGATE 按部门;EXCEPTION_TYPE 可选按部门;CUSTOM 跳过 if (cfg.BindingType == "AGGREGATE" || cfg.BindingType == "EXCEPTION_TYPE") { dto.Breakdown = await BuildDeptBreakdownAsync(list, effectiveDeptGroupBy, cfg.StatMetric); } return dto; } private async Task ResolveConfigAsync(long tenantId, long factoryId, string pageCode, string cellCode) { var rows = await _cfgRep.AsQueryable() .Where(x => x.PageCode == pageCode && x.CellCode == cellCode && ((x.TenantId == 0 && x.FactoryId == 0) || (x.TenantId == tenantId && x.FactoryId == factoryId))) .ToListAsync(); return rows.OrderByDescending(x => x.FactoryId).FirstOrDefault(); } private static (DateTime from, DateTime to) TimeRange(string window) { var now = DateTime.Now; return window switch { "TODAY" => (now.Date, now.Date.AddDays(1)), "LAST_24H" => (now.AddHours(-24), now.AddMinutes(1)), "LAST_7D" => (now.AddDays(-7), now.AddMinutes(1)), "LAST_30D" => (now.AddDays(-30), now.AddMinutes(1)), _ => (now.AddHours(-24), now.AddMinutes(1)), }; } private static IEnumerable ApplyAggregateScope(List events, string? scope) => scope switch { "DOMAIN_DELIVERY" => events.Where(e => e.SceneCode == "S1S7_DELIVERY"), "DOMAIN_PRODUCTION" => events.Where(e => e.SceneCode == "S2S6_PRODUCTION"), "DOMAIN_SUPPLY" => events.Where(e => e.SceneCode == "S3S5_SUPPLY"), "ALL" => events, _ => events, }; private static double ComputeValue(List rows, string metric) { if (rows.Count == 0) return 0; return metric switch { "OPEN_COUNT" => rows.Count(e => e.Status != "CLOSED"), "FREQUENCY" => rows.Count, "AVG_DURATION" => AvgHours(rows), "CLOSE_RATE" => Math.Round(rows.Count(e => e.Status == "CLOSED") * 100.0 / rows.Count, 1), _ => rows.Count, }; } private static double AvgHours(List rows) { var closed = rows.Where(r => r.ClosedAt.HasValue).ToList(); if (closed.Count == 0) return 0; return Math.Round(closed.Average(r => (r.ClosedAt!.Value - r.CreatedAt).TotalHours), 1); } private async Task> BuildDeptBreakdownAsync(List rows, string groupBy, string metric) { Func keySel = groupBy == "OCCUR" ? (EvtRow e) => e.OccurrenceDeptId : (EvtRow e) => e.ResponsibleDeptId; var groups = rows.Where(e => keySel(e) > 0).GroupBy(keySel).ToList(); if (groups.Count == 0) return new(); var deptIds = groups.Select(g => g.Key).Distinct().ToList(); var names = (await _orgRep.AsQueryable().Where(o => deptIds.Contains(o.Id)).Select(o => new { o.Id, o.Name }).ToListAsync()) .ToDictionary(o => o.Id, o => o.Name); return groups.Select(g => { var list = g.ToList(); return new AdoS8CellBreakdownItem { Code = g.Key.ToString(), Label = names.TryGetValue(g.Key, out var n) ? n : $"部门{g.Key}", Value = ComputeValue(list, metric), }; }) .OrderByDescending(i => i.Value) .ToList(); } private class EvtRow { public string Status { get; set; } = string.Empty; public string? ExceptionTypeCode { get; set; } public string? ModuleCode { get; set; } public string? SceneCode { get; set; } public long ResponsibleDeptId { get; set; } public long OccurrenceDeptId { get; set; } public DateTime CreatedAt { get; set; } public DateTime? ClosedAt { get; set; } } }