S8DashboardCellDataService.cs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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. var events = await _exRep.AsQueryable()
  50. .Where(e => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
  51. .Where(e => e.CreatedAt >= from && e.CreatedAt < to)
  52. .Select(e => new EvtRow
  53. {
  54. Status = e.Status,
  55. ExceptionTypeCode = e.ExceptionTypeCode,
  56. ModuleCode = e.ModuleCode,
  57. SceneCode = e.SceneCode,
  58. ResponsibleDeptId = e.ResponsibleDeptId,
  59. OccurrenceDeptId = e.OccurrenceDeptId,
  60. CreatedAt = e.CreatedAt,
  61. ClosedAt = e.ClosedAt,
  62. })
  63. .ToListAsync();
  64. // 按 binding_type 过滤
  65. IEnumerable<EvtRow> scoped = cfg.BindingType switch
  66. {
  67. "EXCEPTION_TYPE" => events.Where(e => !string.IsNullOrEmpty(cfg.ExceptionTypeCode) && e.ExceptionTypeCode == cfg.ExceptionTypeCode),
  68. "AGGREGATE" => ApplyAggregateScope(events, cfg.AggregateScope),
  69. _ => events, // CUSTOM:不按类型过滤,返回同页范围聚合值供前端参考
  70. };
  71. var list = scoped.ToList();
  72. var dto = new AdoS8CellDataDto
  73. {
  74. CellCode = cfg.CellCode,
  75. PageCode = cfg.PageCode,
  76. Title = cfg.CellTitle,
  77. BindingType = cfg.BindingType,
  78. StatMetric = cfg.StatMetric,
  79. TimeWindow = cfg.TimeWindow,
  80. Value = ComputeValue(list, cfg.StatMetric),
  81. };
  82. // 明细分组:AGGREGATE 按部门;EXCEPTION_TYPE 可选按部门;CUSTOM 跳过
  83. if (cfg.BindingType == "AGGREGATE" || cfg.BindingType == "EXCEPTION_TYPE")
  84. {
  85. dto.Breakdown = await BuildDeptBreakdownAsync(list, effectiveDeptGroupBy, cfg.StatMetric);
  86. }
  87. return dto;
  88. }
  89. private async Task<AdoS8DashboardCellConfig?> ResolveConfigAsync(long tenantId, long factoryId, string pageCode, string cellCode)
  90. {
  91. var rows = await _cfgRep.AsQueryable()
  92. .Where(x => x.PageCode == pageCode && x.CellCode == cellCode
  93. && ((x.TenantId == 0 && x.FactoryId == 0)
  94. || (x.TenantId == tenantId && x.FactoryId == factoryId)))
  95. .ToListAsync();
  96. return rows.OrderByDescending(x => x.FactoryId).FirstOrDefault();
  97. }
  98. private static (DateTime from, DateTime to) TimeRange(string window)
  99. {
  100. var now = DateTime.Now;
  101. return window switch
  102. {
  103. "TODAY" => (now.Date, now.Date.AddDays(1)),
  104. "LAST_24H" => (now.AddHours(-24), now.AddMinutes(1)),
  105. "LAST_7D" => (now.AddDays(-7), now.AddMinutes(1)),
  106. "LAST_30D" => (now.AddDays(-30), now.AddMinutes(1)),
  107. _ => (now.AddHours(-24), now.AddMinutes(1)),
  108. };
  109. }
  110. // S8-SCENE-MIGRATE-RUNTIME-CODE-S1S7-1:DOMAIN_x 改为 S1–S7 多 scene OR;
  111. // 同时保留旧复合 scene 兼容(27 行历史 active exception 在 HISTORY-CLEANUP 之前仍引用)。
  112. private static IEnumerable<EvtRow> ApplyAggregateScope(List<EvtRow> events, string? scope) => scope switch
  113. {
  114. "DOMAIN_DELIVERY" => events.Where(e =>
  115. e.SceneCode == S8SceneCode.S1
  116. || e.SceneCode == S8SceneCode.S7
  117. || e.SceneCode == S8SceneCode.S1S7Delivery),
  118. "DOMAIN_PRODUCTION" => events.Where(e =>
  119. e.SceneCode == S8SceneCode.S2
  120. || e.SceneCode == S8SceneCode.S6
  121. || e.SceneCode == S8SceneCode.S2S6Production
  122. || e.SceneCode == "S2S6_QUALITY"),
  123. "DOMAIN_SUPPLY" => events.Where(e =>
  124. e.SceneCode == S8SceneCode.S3
  125. || e.SceneCode == S8SceneCode.S4
  126. || e.SceneCode == S8SceneCode.S5
  127. || e.SceneCode == S8SceneCode.S3S5Supply
  128. || e.SceneCode == S8SceneCode.S4Purchase),
  129. "ALL" => events,
  130. _ => events,
  131. };
  132. private static double ComputeValue(List<EvtRow> rows, string metric)
  133. {
  134. if (rows.Count == 0) return 0;
  135. return metric switch
  136. {
  137. "OPEN_COUNT" => rows.Count(e => e.Status != "CLOSED"),
  138. "FREQUENCY" => rows.Count,
  139. "AVG_DURATION" => AvgHours(rows),
  140. "CLOSE_RATE" => Math.Round(rows.Count(e => e.Status == "CLOSED") * 100.0 / rows.Count, 1),
  141. _ => rows.Count,
  142. };
  143. }
  144. private static double AvgHours(List<EvtRow> rows)
  145. {
  146. var closed = rows.Where(r => r.ClosedAt.HasValue).ToList();
  147. if (closed.Count == 0) return 0;
  148. return Math.Round(closed.Average(r => (r.ClosedAt!.Value - r.CreatedAt).TotalHours), 1);
  149. }
  150. private async Task<List<AdoS8CellBreakdownItem>> BuildDeptBreakdownAsync(List<EvtRow> rows, string groupBy, string metric)
  151. {
  152. Func<EvtRow, long> keySel = groupBy == "OCCUR"
  153. ? (EvtRow e) => e.OccurrenceDeptId
  154. : (EvtRow e) => e.ResponsibleDeptId;
  155. var groups = rows.Where(e => keySel(e) > 0).GroupBy(keySel).ToList();
  156. if (groups.Count == 0) return new();
  157. var deptIds = groups.Select(g => g.Key).Distinct().ToList();
  158. var names = (await _orgRep.AsQueryable().Where(o => deptIds.Contains(o.Id)).Select(o => new { o.Id, o.Name }).ToListAsync())
  159. .ToDictionary(o => o.Id, o => o.Name);
  160. return groups.Select(g =>
  161. {
  162. var list = g.ToList();
  163. return new AdoS8CellBreakdownItem
  164. {
  165. Code = g.Key.ToString(),
  166. Label = names.TryGetValue(g.Key, out var n) ? n : $"部门{g.Key}",
  167. Value = ComputeValue(list, metric),
  168. };
  169. })
  170. .OrderByDescending(i => i.Value)
  171. .ToList();
  172. }
  173. private class EvtRow
  174. {
  175. public string Status { get; set; } = string.Empty;
  176. public string? ExceptionTypeCode { get; set; }
  177. public string? ModuleCode { get; set; }
  178. public string? SceneCode { get; set; }
  179. public long ResponsibleDeptId { get; set; }
  180. public long OccurrenceDeptId { get; set; }
  181. public DateTime CreatedAt { get; set; }
  182. public DateTime? ClosedAt { get; set; }
  183. }
  184. }