S8MonitoringService.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. public class S8MonitoringService : ITransient
  6. {
  7. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  8. private readonly SqlSugarRepository<AdoS8ExceptionType> _typeRep;
  9. private readonly SqlSugarRepository<SysOrg> _orgRep;
  10. public S8MonitoringService(
  11. SqlSugarRepository<AdoS8Exception> rep,
  12. SqlSugarRepository<AdoS8ExceptionType> typeRep,
  13. SqlSugarRepository<SysOrg> orgRep)
  14. {
  15. _rep = rep;
  16. _typeRep = typeRep;
  17. _orgRep = orgRep;
  18. }
  19. /// <summary>
  20. /// 9宫格数据:S1-S7 订单健康分布 + S8业务类别汇总 + S9部门汇总。
  21. /// 数据来源 ado_s8_exception 聚合;异常表为空时返回全 0(不再返回 Demo 数据)。
  22. /// </summary>
  23. public async Task<AdoS8OrderGridDto> GetOrderGridAsync(long tenantId = 1, long factoryId = 1)
  24. {
  25. // S8-DASHBOARD-DATA-ALIGN-S1S7-1:统一统计基准 module_code IN S1-S7;
  26. // module_code IS NULL 的 legacy/demo 行不参与大屏聚合(不依赖 scene_code、不映射 legacy scene)。
  27. var events = await _rep.AsQueryable()
  28. .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
  29. .Where(e => S8ModuleCode.All.Contains(e.ModuleCode))
  30. .Select(e => new
  31. {
  32. e.ModuleCode,
  33. e.Severity,
  34. e.Status,
  35. e.TimeoutFlag,
  36. e.ExceptionTypeCode,
  37. e.SceneCode,
  38. e.ResponsibleDeptId,
  39. e.CreatedAt,
  40. e.ClosedAt
  41. })
  42. .ToListAsync();
  43. // ── Modules:按 module_code 聚合 ──
  44. var modules = S8ModuleCode.All.Select(mc =>
  45. {
  46. var rows = events.Where(e => e.ModuleCode == mc).ToList();
  47. var unclosed = rows.Where(e => e.Status != "CLOSED").ToList();
  48. var red = unclosed.Count(e => e.Severity == "CRITICAL" || e.Severity == "HIGH");
  49. var yellow = unclosed.Count(e => e.Severity == "MEDIUM");
  50. var green = unclosed.Count(e => e.Severity == "LOW");
  51. var closed = rows.Count(e => e.Status == "CLOSED");
  52. var total = rows.Count;
  53. return new AdoS8ModuleOrderSummary
  54. {
  55. ModuleCode = mc,
  56. ModuleLabel = S8ModuleCode.Label(mc),
  57. Green = green,
  58. Yellow = yellow,
  59. Red = red,
  60. Total = total,
  61. Frequency = total,
  62. AvgProcessHours = AvgHours(rows.Select(r => (r.CreatedAt, r.ClosedAt))),
  63. CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1),
  64. };
  65. }).ToList();
  66. // ── ByCategory:按异常类型的 type_code 聚合为 5 大业务类别(CategoryOf) ──
  67. var typeMap = (await _typeRep.AsQueryable()
  68. .Where(t => (t.TenantId == 0 && t.FactoryId == 0)
  69. || (t.TenantId == tenantId && t.FactoryId == factoryId))
  70. .ToListAsync())
  71. .GroupBy(t => t.TypeCode)
  72. .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.FactoryId).First());
  73. var byCategory = events
  74. .Where(e => !string.IsNullOrEmpty(e.ExceptionTypeCode) && typeMap.ContainsKey(e.ExceptionTypeCode!))
  75. .GroupBy(e => CategoryOf(typeMap[e.ExceptionTypeCode!]))
  76. .Where(g => !string.IsNullOrEmpty(g.Key))
  77. .Select(g =>
  78. {
  79. var total = g.Count();
  80. var closed = g.Count(e => e.Status == "CLOSED");
  81. return new AdoS8CategorySummary
  82. {
  83. Category = g.Key!,
  84. Total = total,
  85. AvgProcessHours = AvgHours(g.Select(e => (e.CreatedAt, e.ClosedAt))),
  86. CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1),
  87. };
  88. })
  89. .OrderBy(c => CategoryOrder(c.Category))
  90. .ToList();
  91. // ── ByDept:按 ResponsibleDeptId 聚合,JOIN SysOrg 取部门名称 ──
  92. var deptIds = events.Select(e => e.ResponsibleDeptId).Where(id => id > 0).Distinct().ToList();
  93. var deptNameMap = deptIds.Count == 0
  94. ? new Dictionary<long, string>()
  95. : (await _orgRep.AsQueryable().Where(o => deptIds.Contains(o.Id)).Select(o => new { o.Id, o.Name }).ToListAsync())
  96. .ToDictionary(o => o.Id, o => o.Name);
  97. var byDept = events
  98. .Where(e => e.ResponsibleDeptId > 0)
  99. .GroupBy(e => e.ResponsibleDeptId)
  100. .Select(g =>
  101. {
  102. var total = g.Count();
  103. var closed = g.Count(e => e.Status == "CLOSED");
  104. return new AdoS8DeptSummary
  105. {
  106. DeptName = deptNameMap.TryGetValue(g.Key, out var n) ? n : $"部门{g.Key}",
  107. Total = total,
  108. AvgProcessHours = AvgHours(g.Select(e => (e.CreatedAt, e.ClosedAt))),
  109. CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1),
  110. };
  111. })
  112. .OrderByDescending(d => d.Total)
  113. .ToList();
  114. return new AdoS8OrderGridDto
  115. {
  116. Modules = modules,
  117. ByCategory = byCategory,
  118. ByDept = byDept,
  119. };
  120. }
  121. private static double AvgHours(IEnumerable<(DateTime createdAt, DateTime? closedAt)> items)
  122. {
  123. var closed = items.Where(x => x.closedAt.HasValue).ToList();
  124. if (closed.Count == 0) return 0;
  125. var avg = closed.Average(x => (x.closedAt!.Value - x.createdAt).TotalHours);
  126. return Math.Round(avg, 1);
  127. }
  128. /// <summary>异常类型 → 业务类别(与 Overview 页 5 张类别卡对应)。
  129. /// S8-DEPRECATED-EXCEPTION-TYPE-MIGRATE-1:补 7 个迁移目标 type_code 的归类,并对旧 7 条保留兼容
  130. /// (历史 active 异常迁移期间 / 重建 DB 期间都不丢类别卡)。</summary>
  131. private static string CategoryOf(AdoS8ExceptionType t) => t.TypeCode switch
  132. {
  133. // 订单评审
  134. "ORDER_CHANGE" => "订单评审",
  135. // 总装发货
  136. "DELIVERY_DELAY" => "总装发货",
  137. "PENDING_SHIPMENT" => "总装发货",
  138. // 本体生产(新基线)
  139. "EQUIP_FAULT" => "本体生产",
  140. "MFG_MATERIAL_ABNORMAL" => "本体生产",
  141. "MFG_QUALITY_ABNORMAL" => "本体生产",
  142. "PRODUCTION_MATERIAL_ABNORMAL" => "本体生产",
  143. "PRODUCTION_QUALITY_ABNORMAL" => "本体生产",
  144. "WORK_ORDER_KITTING_ABNORMAL" => "本体生产",
  145. "WORK_ORDER_ISSUE_ABNORMAL" => "本体生产",
  146. // 材料采购(新基线)
  147. "SUPPLIER_ETA_ISSUE" => "材料采购",
  148. "SUPPLIER_SHIP_ISSUE" => "材料采购",
  149. "IQC_ISSUE" => "材料采购",
  150. "WH_PUTAWAY_ISSUE" => "材料采购",
  151. "WAREHOUSE_RECEIPT_ABNORMAL" => "材料采购",
  152. // 旧 7 条 deprecated 仍保留兼容(DB 已迁移;此分支防 Overview 临时回放历史 active)
  153. "MATERIAL_SHORTAGE" => "本体生产",
  154. "QUALITY_DEFECT" => "本体生产",
  155. "DIMENSION_DEVIATION" => "本体生产",
  156. "YIELD_DEFICIT" => "本体生产",
  157. "WH_KIT_ISSUE" => "本体生产",
  158. "WH_ISSUE_OUT_ISSUE" => "本体生产",
  159. "WH_INBOUND_ISSUE" => "材料采购",
  160. _ => string.Empty,
  161. };
  162. private static int CategoryOrder(string category) => category switch
  163. {
  164. "订单评审" => 1,
  165. "产品设计" => 2,
  166. "材料采购" => 3,
  167. "本体生产" => 4,
  168. "总装发货" => 5,
  169. _ => 99,
  170. };
  171. /// <summary>
  172. /// 异常监控汇总:按 module_code 分组统计红/黄/绿/超时数。
  173. /// 供综合全景页顶部徽标和模块汇总表使用。
  174. /// </summary>
  175. public async Task<AdoS8MonitoringSummaryDto> GetSummaryAsync(AdoS8MonitoringSummaryQueryDto q)
  176. {
  177. var query = _rep.AsQueryable()
  178. .Where(e => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
  179. // S8-DASHBOARD-DATA-ALIGN-S1S7-1:统一统计基准 module_code IN S1-S7。
  180. .Where(e => S8ModuleCode.All.Contains(e.ModuleCode))
  181. .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), e => e.SceneCode == q.SceneCode)
  182. .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), e => e.ModuleCode == q.ModuleCode)
  183. .WhereIF(q.BizDateFrom.HasValue, e => e.CreatedAt >= q.BizDateFrom!.Value)
  184. .WhereIF(q.BizDateTo.HasValue, e => e.CreatedAt <= q.BizDateTo!.Value);
  185. // 聚合到内存(数据量在可控范围内,避免复杂 GROUP BY 兼容性问题)
  186. var raw = await query
  187. .Select(e => new
  188. {
  189. e.ModuleCode,
  190. e.SceneCode,
  191. e.Severity,
  192. e.TimeoutFlag,
  193. e.Status
  194. })
  195. .ToListAsync();
  196. var byModule = raw
  197. .GroupBy(e => new { mc = e.ModuleCode ?? string.Empty, sc = e.SceneCode ?? string.Empty })
  198. .Select(g => new AdoS8ModuleSummaryItem
  199. {
  200. ModuleCode = g.Key.mc,
  201. ModuleLabel = S8ModuleCode.Label(g.Key.mc),
  202. SceneCode = g.Key.sc,
  203. SceneLabel = S8SceneCode.Label(g.Key.sc),
  204. Total = g.Count(),
  205. Red = g.Count(e => e.Severity == "CRITICAL" || e.Severity == "HIGH"),
  206. Yellow = g.Count(e => e.Severity == "MEDIUM"),
  207. Green = g.Count(e => e.Severity == "LOW"),
  208. Timeout = g.Count(e => e.TimeoutFlag && e.Status != "CLOSED")
  209. })
  210. // 按 S8ModuleCode.All 顺序排列
  211. .OrderBy(r => Array.IndexOf(S8ModuleCode.All, r.ModuleCode))
  212. .ToList();
  213. return new AdoS8MonitoringSummaryDto
  214. {
  215. Total = raw.Count,
  216. Red = raw.Count(e => e.Severity == "CRITICAL" || e.Severity == "HIGH"),
  217. Yellow = raw.Count(e => e.Severity == "MEDIUM"),
  218. Green = raw.Count(e => e.Severity == "LOW"),
  219. Timeout = raw.Count(e => e.TimeoutFlag && e.Status != "CLOSED"),
  220. ByModule = byModule
  221. };
  222. }
  223. }