S8DashboardService.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
  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 S8DashboardService : ITransient
  7. {
  8. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  9. private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
  10. private readonly SqlSugarRepository<AdoS8ProcessNode> _processNodeRep;
  11. public S8DashboardService(
  12. SqlSugarRepository<AdoS8Exception> rep,
  13. SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
  14. SqlSugarRepository<AdoS8ProcessNode> processNodeRep)
  15. {
  16. _rep = rep;
  17. _deptRep = deptRep;
  18. _processNodeRep = processNodeRep;
  19. }
  20. public async Task<object> GetOverviewAsync(long tenantId, long factoryId, DateTime? beginTime = null, DateTime? endTime = null, string? severity = null)
  21. {
  22. // 看板顶部的"开始/结束日期"通过 beginTime/endTime 透传,与列表的 beginTime/endTime 同语义。
  23. // BUG-12:补 severity 过滤,与异常列表 API 的 severity 口径一致(精确匹配 LOW/MEDIUM/HIGH/CRITICAL)。
  24. // BUG-22:REJECTED 状态归入 pending 桶,避免从总数视角"消失";与列表 statusBucket="pending" 同步对齐。
  25. // S8-DASHBOARD-DATA-ALIGN-S1S7-1:统一只统计 module_code IN S1-S7;NULL module 的 legacy 行不参与看板 KPI。
  26. var q = _rep.AsQueryable()
  27. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted)
  28. .Where(x => S8ModuleCode.All.Contains(x.ModuleCode))
  29. .WhereIF(beginTime.HasValue, x => x.CreatedAt >= beginTime!.Value)
  30. .WhereIF(endTime.HasValue, x => x.CreatedAt <= endTime!.Value)
  31. .WhereIF(!string.IsNullOrWhiteSpace(severity), x => x.Severity == severity);
  32. var total = await q.CountAsync();
  33. var pending = await q.CountAsync(x => x.Status == "NEW" || x.Status == "ASSIGNED" || x.Status == "IN_PROGRESS" || x.Status == "PENDING_VERIFICATION" || x.Status == "REJECTED");
  34. var inProgress = await q.CountAsync(x => x.Status == "IN_PROGRESS");
  35. var timeout = await q.CountAsync(x => x.TimeoutFlag);
  36. var closed = await q.CountAsync(x => x.Status == "CLOSED");
  37. var todayNew = await q.CountAsync(x => x.CreatedAt >= DateTime.Today);
  38. var critical = await q.CountAsync(x => x.Severity == "CRITICAL");
  39. var closureRate = total > 0 ? Math.Round(closed * 100.0 / total, 1) : 0.0;
  40. // 平均处理周期(小时),仅对已闭环且有关闭时间的记录计算
  41. var closedRows = await q
  42. .Where(x => x.Status == "CLOSED" && x.ClosedAt != null)
  43. .Select(x => new { x.CreatedAt, x.ClosedAt })
  44. .ToListAsync();
  45. var avgCycleHours = closedRows.Count > 0
  46. ? Math.Round(closedRows.Average(x => (x.ClosedAt!.Value - x.CreatedAt).TotalHours), 1)
  47. : 0.0;
  48. return new { total, pending, inProgress, timeout, closed, todayNew, critical, closureRate, avgCycleHours };
  49. }
  50. public async Task<object> GetTrendsAsync(long tenantId, long factoryId, int days)
  51. {
  52. var from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90));
  53. var rows = await _rep.AsQueryable()
  54. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from)
  55. .Where(x => S8ModuleCode.All.Contains(x.ModuleCode))
  56. .Select(x => new { x.CreatedAt })
  57. .ToListAsync();
  58. return rows
  59. .GroupBy(x => x.CreatedAt.Date)
  60. .OrderBy(g => g.Key)
  61. .Select(g => new { date = g.Key.ToString("yyyy-MM-dd"), count = g.Count() })
  62. .ToList();
  63. }
  64. public async Task<object> GetDistributionsAsync(long tenantId, long factoryId)
  65. {
  66. var list = await _rep.AsQueryable()
  67. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted)
  68. .Where(x => S8ModuleCode.All.Contains(x.ModuleCode))
  69. .Select(x => new
  70. {
  71. x.Status,
  72. x.SceneCode,
  73. x.Severity,
  74. x.ResponsibleDeptId,
  75. x.OccurrenceDeptId,
  76. x.ProcessNodeCode,
  77. x.RelatedObjectCode,
  78. })
  79. .ToListAsync();
  80. // 部门名称字典
  81. var allDeptIds = list.Select(x => x.ResponsibleDeptId)
  82. .Concat(list.Select(x => x.OccurrenceDeptId))
  83. .Distinct().ToList();
  84. var deptMap = await _deptRep.AsQueryable()
  85. .Where(d => allDeptIds.Contains(d.Id))
  86. .Select(d => new { d.Id, Name = d.Descr ?? d.Department })
  87. .ToListAsync();
  88. var deptDict = deptMap.ToDictionary(d => d.Id, d => d.Name ?? d.Id.ToString());
  89. // 流程节点名称字典
  90. var processNodes = await _processNodeRep.AsQueryable()
  91. .OrderBy(p => p.SortNo)
  92. .Select(p => new { p.Code, p.Name })
  93. .ToListAsync();
  94. var processDict = processNodes.ToDictionary(p => p.Code, p => p.Name);
  95. return new
  96. {
  97. byStatus = list
  98. .GroupBy(x => x.Status)
  99. .Select(g => new { key = g.Key, count = g.Count() }),
  100. byScene = list
  101. .GroupBy(x => x.SceneCode)
  102. .Select(g => new { key = g.Key, count = g.Count() }),
  103. bySeverity = list
  104. .GroupBy(x => x.Severity)
  105. .Select(g => new { key = g.Key, count = g.Count() }),
  106. byDept = list
  107. .GroupBy(x => x.ResponsibleDeptId)
  108. .Select(g => new
  109. {
  110. key = g.Key,
  111. deptName = deptDict.GetValueOrDefault(g.Key, g.Key.ToString()),
  112. count = g.Count(),
  113. }),
  114. byOccurrenceDept = list
  115. .GroupBy(x => x.OccurrenceDeptId)
  116. .Select(g => new
  117. {
  118. key = g.Key,
  119. deptName = deptDict.GetValueOrDefault(g.Key, g.Key.ToString()),
  120. count = g.Count(),
  121. }),
  122. byProcess = list
  123. .Where(x => x.ProcessNodeCode != null)
  124. .GroupBy(x => x.ProcessNodeCode!)
  125. .Select(g => new
  126. {
  127. key = g.Key,
  128. nodeName = processDict.GetValueOrDefault(g.Key, g.Key),
  129. count = g.Count(),
  130. }),
  131. byObject = list
  132. .Where(x => x.RelatedObjectCode != null)
  133. .GroupBy(x => x.RelatedObjectCode!)
  134. .OrderByDescending(g => g.Count())
  135. .Take(20)
  136. .Select(g => new { key = g.Key, count = g.Count() }),
  137. };
  138. }
  139. public async Task<object> GetDeptBacklogAsync(long tenantId, long factoryId)
  140. {
  141. var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS" };
  142. var list = await _rep.AsQueryable()
  143. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted)
  144. .Where(x => S8ModuleCode.All.Contains(x.ModuleCode))
  145. .Select(x => new { x.ResponsibleDeptId, x.Status, x.TimeoutFlag })
  146. .ToListAsync();
  147. var deptIds = list.Select(x => x.ResponsibleDeptId).Distinct().ToList();
  148. var deptMap = await _deptRep.AsQueryable()
  149. .Where(d => deptIds.Contains(d.Id))
  150. .Select(d => new { d.Id, Name = d.Descr ?? d.Department })
  151. .ToListAsync();
  152. var deptDict = deptMap.ToDictionary(d => d.Id, d => d.Name ?? d.Id.ToString());
  153. return list
  154. .GroupBy(x => x.ResponsibleDeptId)
  155. .Select(g => new
  156. {
  157. deptId = g.Key,
  158. deptName = deptDict.GetValueOrDefault(g.Key, g.Key.ToString()),
  159. pending = g.Count(x => pendingStatuses.Contains(x.Status)),
  160. inProgress = g.Count(x => x.Status == "IN_PROGRESS"),
  161. timeout = g.Count(x => x.TimeoutFlag),
  162. total = g.Count(),
  163. })
  164. .OrderByDescending(x => x.pending)
  165. .ToList();
  166. }
  167. /// <summary>
  168. /// 按维度返回多系列日趋势数据。dim: object | process | occDept | respDept
  169. /// 返回: { dates, series: [{ name, data[] }] }
  170. /// </summary>
  171. public async Task<object> GetDimTrendsAsync(long tenantId, long factoryId, string dim, int days)
  172. {
  173. var from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90));
  174. var rows = await _rep.AsQueryable()
  175. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from)
  176. .Where(x => S8ModuleCode.All.Contains(x.ModuleCode))
  177. .Select(x => new
  178. {
  179. x.CreatedAt,
  180. x.ProcessNodeCode,
  181. x.RelatedObjectCode,
  182. x.OccurrenceDeptId,
  183. x.ResponsibleDeptId,
  184. })
  185. .ToListAsync();
  186. // 生成连续日期序列
  187. var dates = Enumerable.Range(0, (DateTime.Today - from).Days + 1)
  188. .Select(i => from.AddDays(i).Date)
  189. .ToList();
  190. var dateLabels = dates.Select(d => d.ToString("yyyy-MM-dd")).ToList();
  191. // 按维度取 key 函数
  192. Func<dynamic, string> keySelector = dim switch
  193. {
  194. "process" => r => r.ProcessNodeCode ?? "未设置",
  195. "occDept" => r => r.OccurrenceDeptId.ToString(),
  196. "respDept" => r => r.ResponsibleDeptId.ToString(),
  197. _ => r => r.RelatedObjectCode ?? "未设置", // object
  198. };
  199. var grouped = rows
  200. .GroupBy(r => keySelector(r))
  201. .OrderByDescending(g => g.Count())
  202. .Take(8) // 最多取 8 个系列,避免图例过多
  203. .ToList();
  204. // 补全部门名 / 流程节点名
  205. Dictionary<string, string> nameMap = new();
  206. if (dim == "process")
  207. {
  208. var nodes = await _processNodeRep.AsQueryable().ToListAsync();
  209. nameMap = nodes.ToDictionary(n => n.Code, n => n.Name);
  210. }
  211. else if (dim is "occDept" or "respDept")
  212. {
  213. var ids = grouped.Select(g => long.TryParse(g.Key, out var id) ? id : 0).ToList();
  214. var depts = await _deptRep.AsQueryable()
  215. .Where(d => ids.Contains(d.Id))
  216. .Select(d => new { d.Id, Name = d.Descr ?? d.Department })
  217. .ToListAsync();
  218. nameMap = depts.ToDictionary(d => d.Id.ToString(), d => d.Name ?? d.Id.ToString());
  219. }
  220. var series = grouped.Select(g =>
  221. {
  222. var seriesName = nameMap.TryGetValue(g.Key, out var n) ? n : g.Key;
  223. var byDate = g.GroupBy(r => r.CreatedAt.Date).ToDictionary(x => x.Key, x => x.Count());
  224. var data = dates.Select(d => byDate.TryGetValue(d, out var c) ? c : 0).ToList();
  225. return new { name = seriesName, data };
  226. }).ToList();
  227. return new { dates = dateLabels, series };
  228. }
  229. public async Task<List<AdoS8Exception>> GetQuickExceptionsAsync(long tenantId, long factoryId, string mode)
  230. {
  231. var q = _rep.AsQueryable()
  232. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted)
  233. .Where(x => S8ModuleCode.All.Contains(x.ModuleCode));
  234. var ordered = mode switch
  235. {
  236. "high-priority" => q.OrderBy(x => x.PriorityScore, OrderByType.Desc),
  237. "timeout" => q.Where(x => x.TimeoutFlag).OrderBy(x => x.SlaDeadline),
  238. _ => q.OrderBy(x => x.CreatedAt, OrderByType.Desc),
  239. };
  240. return await ordered.Take(20).ToListAsync();
  241. }
  242. }