S8DashboardCellDataService.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. using Admin.NET.Plugin.AiDOP.Dto.S8;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  4. /// <summary>
  5. /// 大屏卡片数据查询服务:按 (pageCode, cellCode) 读取配置,按 binding_type 分发组装结果。
  6. /// 配置解析:工厂覆盖优先(tenant+factory 精确匹配)→ 全局基线(tenant=0/factory=0)兜底。
  7. /// </summary>
  8. public class S8DashboardCellDataService : ITransient
  9. {
  10. private readonly SqlSugarRepository<AdoS8DashboardCellConfig> _cfgRep;
  11. private readonly SqlSugarRepository<AdoS8Exception> _exRep;
  12. private readonly SqlSugarRepository<SysOrg> _orgRep;
  13. public S8DashboardCellDataService(
  14. SqlSugarRepository<AdoS8DashboardCellConfig> cfgRep,
  15. SqlSugarRepository<AdoS8Exception> exRep,
  16. SqlSugarRepository<SysOrg> orgRep)
  17. {
  18. _cfgRep = cfgRep;
  19. _exRep = exRep;
  20. _orgRep = orgRep;
  21. }
  22. public async Task<AdoS8CellDataDto> GetAsync(AdoS8CellDataQueryDto q)
  23. {
  24. if (string.IsNullOrWhiteSpace(q.PageCode) || string.IsNullOrWhiteSpace(q.CellCode))
  25. throw new S8BizException("pageCode 和 cellCode 必填");
  26. var cfg = await ResolveConfigAsync(q.TenantId, q.FactoryId, q.PageCode, q.CellCode);
  27. if (cfg is null)
  28. return new AdoS8CellDataDto
  29. {
  30. PageCode = q.PageCode,
  31. CellCode = q.CellCode,
  32. Message = "未配置",
  33. };
  34. if (!cfg.Enabled)
  35. return new AdoS8CellDataDto
  36. {
  37. PageCode = q.PageCode,
  38. CellCode = q.CellCode,
  39. BindingType = cfg.BindingType,
  40. Title = cfg.CellTitle,
  41. StatMetric = cfg.StatMetric,
  42. TimeWindow = cfg.TimeWindow,
  43. Message = "已禁用",
  44. };
  45. var (from, to) = TimeRange(cfg.TimeWindow);
  46. var effectiveDeptGroupBy = !string.IsNullOrWhiteSpace(q.DeptGroupBy) ? q.DeptGroupBy! : cfg.DeptGroupBy;
  47. // 取事实数据
  48. var events = await _exRep.AsQueryable()
  49. .Where(e => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
  50. .Where(e => e.CreatedAt >= from && e.CreatedAt < to)
  51. .Select(e => new EvtRow
  52. {
  53. Status = e.Status,
  54. ExceptionTypeCode = e.ExceptionTypeCode,
  55. ModuleCode = e.ModuleCode,
  56. SceneCode = e.SceneCode,
  57. ResponsibleDeptId = e.ResponsibleDeptId,
  58. OccurrenceDeptId = e.OccurrenceDeptId,
  59. CreatedAt = e.CreatedAt,
  60. ClosedAt = e.ClosedAt,
  61. })
  62. .ToListAsync();
  63. // 按 binding_type 过滤
  64. IEnumerable<EvtRow> scoped = cfg.BindingType switch
  65. {
  66. "EXCEPTION_TYPE" => events.Where(e => !string.IsNullOrEmpty(cfg.ExceptionTypeCode) && e.ExceptionTypeCode == cfg.ExceptionTypeCode),
  67. "AGGREGATE" => ApplyAggregateScope(events, cfg.AggregateScope),
  68. _ => events, // CUSTOM:不按类型过滤,返回同页范围聚合值供前端参考
  69. };
  70. var list = scoped.ToList();
  71. var dto = new AdoS8CellDataDto
  72. {
  73. CellCode = cfg.CellCode,
  74. PageCode = cfg.PageCode,
  75. Title = cfg.CellTitle,
  76. BindingType = cfg.BindingType,
  77. StatMetric = cfg.StatMetric,
  78. TimeWindow = cfg.TimeWindow,
  79. Value = ComputeValue(list, cfg.StatMetric),
  80. };
  81. // 明细分组:AGGREGATE 按部门;EXCEPTION_TYPE 可选按部门;CUSTOM 跳过
  82. if (cfg.BindingType == "AGGREGATE" || cfg.BindingType == "EXCEPTION_TYPE")
  83. {
  84. dto.Breakdown = await BuildDeptBreakdownAsync(list, effectiveDeptGroupBy, cfg.StatMetric);
  85. }
  86. return dto;
  87. }
  88. private async Task<AdoS8DashboardCellConfig?> ResolveConfigAsync(long tenantId, long factoryId, string pageCode, string cellCode)
  89. {
  90. var rows = await _cfgRep.AsQueryable()
  91. .Where(x => x.PageCode == pageCode && x.CellCode == cellCode
  92. && ((x.TenantId == 0 && x.FactoryId == 0)
  93. || (x.TenantId == tenantId && x.FactoryId == factoryId)))
  94. .ToListAsync();
  95. return rows.OrderByDescending(x => x.FactoryId).FirstOrDefault();
  96. }
  97. private static (DateTime from, DateTime to) TimeRange(string window)
  98. {
  99. var now = DateTime.Now;
  100. return window switch
  101. {
  102. "TODAY" => (now.Date, now.Date.AddDays(1)),
  103. "LAST_24H" => (now.AddHours(-24), now.AddMinutes(1)),
  104. "LAST_7D" => (now.AddDays(-7), now.AddMinutes(1)),
  105. "LAST_30D" => (now.AddDays(-30), now.AddMinutes(1)),
  106. _ => (now.AddHours(-24), now.AddMinutes(1)),
  107. };
  108. }
  109. private static IEnumerable<EvtRow> ApplyAggregateScope(List<EvtRow> events, string? scope) => scope switch
  110. {
  111. "DOMAIN_DELIVERY" => events.Where(e => e.SceneCode == "S1S7_DELIVERY"),
  112. "DOMAIN_PRODUCTION" => events.Where(e => e.SceneCode == "S2S6_PRODUCTION"),
  113. "DOMAIN_SUPPLY" => events.Where(e => e.SceneCode == "S3S5_SUPPLY"),
  114. "ALL" => events,
  115. _ => events,
  116. };
  117. private static double ComputeValue(List<EvtRow> rows, string metric)
  118. {
  119. if (rows.Count == 0) return 0;
  120. return metric switch
  121. {
  122. "OPEN_COUNT" => rows.Count(e => e.Status != "CLOSED"),
  123. "FREQUENCY" => rows.Count,
  124. "AVG_DURATION" => AvgHours(rows),
  125. "CLOSE_RATE" => Math.Round(rows.Count(e => e.Status == "CLOSED") * 100.0 / rows.Count, 1),
  126. _ => rows.Count,
  127. };
  128. }
  129. private static double AvgHours(List<EvtRow> rows)
  130. {
  131. var closed = rows.Where(r => r.ClosedAt.HasValue).ToList();
  132. if (closed.Count == 0) return 0;
  133. return Math.Round(closed.Average(r => (r.ClosedAt!.Value - r.CreatedAt).TotalHours), 1);
  134. }
  135. private async Task<List<AdoS8CellBreakdownItem>> BuildDeptBreakdownAsync(List<EvtRow> rows, string groupBy, string metric)
  136. {
  137. Func<EvtRow, long> keySel = groupBy == "OCCUR"
  138. ? (EvtRow e) => e.OccurrenceDeptId
  139. : (EvtRow e) => e.ResponsibleDeptId;
  140. var groups = rows.Where(e => keySel(e) > 0).GroupBy(keySel).ToList();
  141. if (groups.Count == 0) return new();
  142. var deptIds = groups.Select(g => g.Key).Distinct().ToList();
  143. var names = (await _orgRep.AsQueryable().Where(o => deptIds.Contains(o.Id)).Select(o => new { o.Id, o.Name }).ToListAsync())
  144. .ToDictionary(o => o.Id, o => o.Name);
  145. return groups.Select(g =>
  146. {
  147. var list = g.ToList();
  148. return new AdoS8CellBreakdownItem
  149. {
  150. Code = g.Key.ToString(),
  151. Label = names.TryGetValue(g.Key, out var n) ? n : $"部门{g.Key}",
  152. Value = ComputeValue(list, metric),
  153. };
  154. })
  155. .OrderByDescending(i => i.Value)
  156. .ToList();
  157. }
  158. private class EvtRow
  159. {
  160. public string Status { get; set; } = string.Empty;
  161. public string? ExceptionTypeCode { get; set; }
  162. public string? ModuleCode { get; set; }
  163. public string? SceneCode { get; set; }
  164. public long ResponsibleDeptId { get; set; }
  165. public long OccurrenceDeptId { get; set; }
  166. public DateTime CreatedAt { get; set; }
  167. public DateTime? ClosedAt { get; set; }
  168. }
  169. }