S8DashboardService.cs 11 KB

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