S8DashboardService.cs 11 KB

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