S8MonitoringService.cs 12 KB

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