S8DashboardCellDataService.cs 8.0 KB

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