AidopWorkbenchService.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity;
  3. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  4. using Admin.NET.Plugin.AiDOP.Entity.S8;
  5. using Admin.NET.Plugin.AiDOP.Infrastructure;
  6. using Admin.NET.Plugin.AiDOP.SmartOps;
  7. using Admin.NET.Plugin.AiDOP.Workbench.Dto;
  8. using Admin.NET.Plugin.ApprovalFlow;
  9. using Admin.NET.Plugin.ApprovalFlow.Service;
  10. using Microsoft.Extensions.Logging;
  11. namespace Admin.NET.Plugin.AiDOP.Workbench;
  12. public sealed class AidopWorkbenchService : ITransient
  13. {
  14. private readonly ISqlSugarClient _db;
  15. private readonly SmartOpsTrustedScopeResolver _trustedScope;
  16. private readonly FlowTaskService _flowTasks;
  17. private readonly ILogger<AidopWorkbenchService> _logger;
  18. public AidopWorkbenchService(
  19. ISqlSugarClient db,
  20. SmartOpsTrustedScopeResolver trustedScope,
  21. FlowTaskService flowTasks,
  22. ILogger<AidopWorkbenchService> logger)
  23. {
  24. _db = db;
  25. _trustedScope = trustedScope;
  26. _flowTasks = flowTasks;
  27. _logger = logger;
  28. }
  29. public async Task<AidopWorkbenchSummaryDto> GetSummaryAsync(int top)
  30. {
  31. var scope = await _trustedScope.ResolveAsync();
  32. top = Math.Clamp(top <= 0 ? 6 : top, 1, 20);
  33. var dto = new AidopWorkbenchSummaryDto
  34. {
  35. GeneratedAt = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:sszzz"),
  36. };
  37. dto.ImprovementActions = await SafeSection("improvement", () => LoadImprovementAsync(scope, top, dto.Counts));
  38. dto.ApprovalTasks = await SafeSection("approval", () => LoadApprovalsAsync(top, dto.Counts));
  39. dto.S8Tasks = await SafeSection("s8", () => LoadS8Async(scope, top, dto.Counts));
  40. dto.UnreadNotices = await SafeSection("notice", () => LoadNoticesAsync(scope, top, dto.Counts));
  41. dto.Pending = MergePending(dto, top);
  42. dto.Counts.PendingTotal = dto.Pending.Total;
  43. dto.Initiated = await SafeSection("initiated", () => LoadInitiatedAsync(scope, top, dto.Counts));
  44. dto.Completed = await SafeSection("completed", () => LoadCompletedAsync(scope, top, dto.Counts));
  45. return dto;
  46. }
  47. private async Task<AidopWorkbenchSectionDto> SafeSection(string name, Func<Task<AidopWorkbenchSectionDto>> factory)
  48. {
  49. try
  50. {
  51. return await factory();
  52. }
  53. catch (Exception ex)
  54. {
  55. _logger.LogWarning(ex, "工作台分区加载失败 section={Section}", name);
  56. return new AidopWorkbenchSectionDto
  57. {
  58. Available = false,
  59. ErrorCode = "SECTION_FAILED",
  60. Message = "该分区暂时不可用",
  61. };
  62. }
  63. }
  64. private async Task<AidopWorkbenchSectionDto> LoadImprovementAsync(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
  65. {
  66. var today = DateTime.Today;
  67. var q = _db.Queryable<AdoSmartOpsImprovementAction>()
  68. .Where(x => x.TenantId == scope.TenantId && x.OwnerUserId == scope.UserId)
  69. .Where(x => x.Status == SmartOpsImprovementActionStatus.Pending
  70. || x.Status == SmartOpsImprovementActionStatus.InProgress);
  71. var open = await q.Clone().CountAsync();
  72. var dueSoon = await q.Clone()
  73. .Where(x => x.DueDate != null && x.DueDate.Value.Date >= today && x.DueDate.Value.Date <= today.AddDays(1))
  74. .CountAsync();
  75. var overdue = await q.Clone()
  76. .Where(x => x.DueDate != null && x.DueDate.Value.Date < today)
  77. .CountAsync();
  78. counts.OverdueTotal += overdue;
  79. counts.DueToday += await q.Clone()
  80. .Where(x => x.DueDate != null && x.DueDate.Value >= today && x.DueDate.Value < today.AddDays(1))
  81. .CountAsync();
  82. counts.ImprovementOpen = open;
  83. counts.ImprovementDueSoon = dueSoon;
  84. counts.ImprovementOverdue = overdue;
  85. var rows = await q.Clone().OrderBy(x => x.DueDate).Take(top).ToListAsync();
  86. var planIds = rows.Select(x => x.PlanId).Distinct().ToList();
  87. var plans = planIds.Count == 0
  88. ? new List<AdoSmartOpsImprovementPlan>()
  89. : await _db.Queryable<AdoSmartOpsImprovementPlan>()
  90. .Where(x => x.TenantId == scope.TenantId && planIds.Contains(x.Id))
  91. .ToListAsync();
  92. var planMap = plans.ToDictionary(x => x.Id, x => x);
  93. return new AidopWorkbenchSectionDto
  94. {
  95. Total = open,
  96. Items = rows.Select(x =>
  97. {
  98. planMap.TryGetValue(x.PlanId, out var plan);
  99. return new AidopWorkbenchItemDto
  100. {
  101. Id = x.Id.ToString(),
  102. Source = "SmartOpsAction",
  103. Title = x.Content,
  104. Status = SmartOpsImprovementActionStatus.DisplayStatus(x.Status, x.DueDate, today),
  105. DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
  106. Overdue = SmartOpsImprovementActionStatus.IsOverdue(x.Status, x.DueDate, today),
  107. PlanId = x.PlanId.ToString(),
  108. PlanNo = plan?.PlanNo,
  109. Category = "改善行动",
  110. CreatedAt = x.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  111. JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.PlanId}&actionId={x.Id}",
  112. };
  113. }).ToList(),
  114. };
  115. }
  116. private async Task<AidopWorkbenchSectionDto> LoadApprovalsAsync(int top, AidopWorkbenchCountsDto counts)
  117. {
  118. counts.ApprovalPending = await _flowTasks.MyPendingCount();
  119. var page = await _flowTasks.MyPendingPage(new TaskPageInput { Page = 1, PageSize = top });
  120. return new AidopWorkbenchSectionDto
  121. {
  122. Total = counts.ApprovalPending,
  123. Items = (page.Items ?? new List<TaskPageOutput>()).Select(t => new AidopWorkbenchItemDto
  124. {
  125. Id = t.Id.ToString(),
  126. Source = "Approval",
  127. Title = string.IsNullOrWhiteSpace(t.Title) ? t.BizType : t.Title,
  128. Status = "pending",
  129. Extra = t.BizType,
  130. Category = "审批",
  131. CreatedAt = t.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  132. JumpUrl = ApprovalJumpUrl(t),
  133. }).ToList(),
  134. };
  135. }
  136. private async Task<AidopWorkbenchSectionDto> LoadS8Async(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
  137. {
  138. var recIds = await _db.Queryable<AdoS0EmployeeMaster>().ClearFilter()
  139. .Where(x => x.SysUserId == scope.UserId)
  140. .Select(x => x.Id)
  141. .ToListAsync();
  142. if (recIds.Count == 0)
  143. {
  144. return new AidopWorkbenchSectionDto
  145. {
  146. Available = false,
  147. ErrorCode = "EMPLOYEE_UNBOUND",
  148. Message = "当前账号未绑定员工主数据,无法列出个人 S8 事项",
  149. };
  150. }
  151. var assignedQ = _db.Queryable<AdoS8Exception>()
  152. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.AssigneeId ?? 0))
  153. .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED");
  154. var verifyQ = _db.Queryable<AdoS8Exception>()
  155. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.VerifierId ?? 0))
  156. .Where(x => x.Status == "PENDING_VERIFICATION");
  157. counts.S8Assigned = await assignedQ.Clone().CountAsync();
  158. counts.S8Verification = await verifyQ.Clone().CountAsync();
  159. var today = DateTime.Today;
  160. counts.DueToday += await _db.Queryable<AdoS8Exception>()
  161. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
  162. .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED")
  163. .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0))
  164. .Where(x => x.SlaDeadline != null && x.SlaDeadline >= today && x.SlaDeadline < today.AddDays(1))
  165. .CountAsync();
  166. counts.OverdueTotal += await _db.Queryable<AdoS8Exception>()
  167. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
  168. .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED")
  169. .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0))
  170. .Where(x => x.TimeoutFlag || (x.SlaDeadline != null && x.SlaDeadline < DateTime.Now))
  171. .CountAsync();
  172. var assigned = await assignedQ.Clone().OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(top).ToListAsync();
  173. var verify = await verifyQ.Clone().OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(top).ToListAsync();
  174. var merged = assigned.Concat(verify)
  175. .GroupBy(x => x.Id)
  176. .Select(g => g.First())
  177. .Take(top)
  178. .ToList();
  179. return new AidopWorkbenchSectionDto
  180. {
  181. Total = counts.S8Assigned + counts.S8Verification,
  182. Items = merged.Select(x => new AidopWorkbenchItemDto
  183. {
  184. Id = x.Id.ToString(),
  185. Source = "S8Exception",
  186. Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
  187. Status = x.Status,
  188. DueDate = x.SlaDeadline?.ToString("yyyy-MM-dd"),
  189. Overdue = x.TimeoutFlag || (x.SlaDeadline.HasValue && x.SlaDeadline < DateTime.Now && x.Status != "CLOSED"),
  190. Extra = x.ExceptionCode,
  191. Category = "异常",
  192. CreatedAt = x.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
  193. JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
  194. }).ToList(),
  195. };
  196. }
  197. private async Task<AidopWorkbenchSectionDto> LoadNoticesAsync(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
  198. {
  199. var q = _db.Queryable<SysNoticeUser>()
  200. .Includes(u => u.SysNotice)
  201. .Where(u => u.UserId == scope.UserId && u.ReadStatus == NoticeUserStatusEnum.UNREAD);
  202. counts.NoticeUnread = await q.Clone().CountAsync();
  203. var rows = await q.Clone()
  204. .OrderByDescending(u => u.SysNotice.CreateTime)
  205. .Take(top)
  206. .ToListAsync();
  207. return new AidopWorkbenchSectionDto
  208. {
  209. Total = counts.NoticeUnread,
  210. Items = rows.Where(x => x.SysNotice != null).Select(x => new AidopWorkbenchItemDto
  211. {
  212. Id = x.SysNotice.Id.ToString(),
  213. Source = "SysNotice",
  214. Title = x.SysNotice.Title,
  215. Status = "unread",
  216. Extra = x.SysNotice.PublicTime?.ToString("yyyy-MM-dd HH:mm"),
  217. Category = "消息",
  218. CreatedAt = x.SysNotice.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  219. JumpUrl = $"/dashboard/notice?noticeId={x.SysNotice.Id}",
  220. }).ToList(),
  221. };
  222. }
  223. private static AidopWorkbenchSectionDto MergePending(AidopWorkbenchSummaryDto dto, int top)
  224. {
  225. var sections = new[] { dto.ImprovementActions, dto.ApprovalTasks, dto.S8Tasks };
  226. var items = sections.SelectMany(x => x.Items)
  227. .OrderByDescending(x => x.Overdue)
  228. .ThenBy(x => ParseDate(x.DueDate) ?? DateTime.MaxValue)
  229. .ThenByDescending(x => ParseDate(x.CreatedAt) ?? DateTime.MinValue)
  230. .Take(top)
  231. .ToList();
  232. return new AidopWorkbenchSectionDto
  233. {
  234. Available = sections.Any(x => x.Available),
  235. Total = sections.Sum(x => x.Total),
  236. Items = items,
  237. Message = sections.All(x => !x.Available) ? "待办数据暂时不可用" : null,
  238. };
  239. }
  240. private async Task<AidopWorkbenchSectionDto> LoadInitiatedAsync(
  241. SmartOpsTrustedScope scope,
  242. int top,
  243. AidopWorkbenchCountsDto counts)
  244. {
  245. var actionPlanIds = await _db.Queryable<AdoSmartOpsImprovementAction>()
  246. .Where(x => x.TenantId == scope.TenantId && x.CreateUserId == scope.UserId)
  247. .Select(x => x.PlanId)
  248. .Distinct()
  249. .ToListAsync();
  250. var planCount = actionPlanIds.Count;
  251. var plans = actionPlanIds.Count == 0
  252. ? new List<AdoSmartOpsImprovementPlan>()
  253. : await _db.Queryable<AdoSmartOpsImprovementPlan>()
  254. .Where(x => x.TenantId == scope.TenantId && actionPlanIds.Contains(x.Id))
  255. .OrderByDescending(x => x.CreateTime)
  256. .Take(top)
  257. .ToListAsync();
  258. var recIds = await ResolveEmployeeIds(scope.UserId);
  259. var reportedQ = _db.Queryable<AdoS8Exception>()
  260. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.ReporterId ?? 0));
  261. var reportedCount = recIds.Count == 0 ? 0 : await reportedQ.Clone().CountAsync();
  262. var reported = recIds.Count == 0
  263. ? new List<AdoS8Exception>()
  264. : await reportedQ.Clone().OrderByDescending(x => x.CreatedAt).Take(top).ToListAsync();
  265. var flowQ = _db.Queryable<ApprovalFlowInstance>()
  266. .Where(x => x.InitiatorId == scope.UserId)
  267. .Where(x => x.BizType != "SMART_OPS_IMPROVEMENT" && !x.BizType.StartsWith("EXCEPTION_"));
  268. var flowCount = await flowQ.Clone().CountAsync();
  269. var flows = await flowQ.Clone().OrderByDescending(x => x.StartTime).Take(top).ToListAsync();
  270. var items = plans.Select(x => new AidopWorkbenchItemDto
  271. {
  272. Id = x.Id.ToString(),
  273. Source = "ImprovementPlan",
  274. Category = "改善计划",
  275. Title = string.IsNullOrWhiteSpace(x.ProblemName) ? x.PlanNo : x.ProblemName,
  276. Status = x.Status,
  277. DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
  278. PlanId = x.Id.ToString(),
  279. PlanNo = x.PlanNo,
  280. CreatedAt = x.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  281. JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.Id}",
  282. })
  283. .Concat(reported.Select(x => new AidopWorkbenchItemDto
  284. {
  285. Id = x.Id.ToString(),
  286. Source = "S8Reported",
  287. Category = "异常提报",
  288. Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
  289. Status = x.Status,
  290. DueDate = x.SlaDeadline?.ToString("yyyy-MM-dd"),
  291. Extra = x.ExceptionCode,
  292. CreatedAt = x.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
  293. JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
  294. }))
  295. .Concat(flows.Select(x => new AidopWorkbenchItemDto
  296. {
  297. Id = x.Id.ToString(),
  298. Source = "ApprovalInitiated",
  299. Category = "审批发起",
  300. Title = x.Title,
  301. Status = x.Status.ToString(),
  302. Extra = x.BizType,
  303. CreatedAt = x.StartTime.ToString("yyyy-MM-dd HH:mm"),
  304. CompletedAt = x.EndTime?.ToString("yyyy-MM-dd HH:mm"),
  305. JumpUrl = "/aidop/flowManage/approvalFlowCenter",
  306. }))
  307. .OrderByDescending(x => ParseDate(x.CreatedAt) ?? DateTime.MinValue)
  308. .Take(top)
  309. .ToList();
  310. counts.InitiatedTotal = planCount + reportedCount + flowCount;
  311. return new AidopWorkbenchSectionDto { Total = counts.InitiatedTotal, Items = items };
  312. }
  313. private async Task<AidopWorkbenchSectionDto> LoadCompletedAsync(
  314. SmartOpsTrustedScope scope,
  315. int top,
  316. AidopWorkbenchCountsDto counts)
  317. {
  318. var actionsQ = _db.Queryable<AdoSmartOpsImprovementAction>()
  319. .Where(x => x.TenantId == scope.TenantId && x.OwnerUserId == scope.UserId)
  320. .Where(x => x.Status == SmartOpsImprovementActionStatus.Completed);
  321. var actionCount = await actionsQ.Clone().CountAsync();
  322. var actions = await actionsQ.Clone()
  323. .OrderByDescending(x => x.CompletedAt ?? x.UpdateTime)
  324. .Take(top)
  325. .ToListAsync();
  326. var recIds = await ResolveEmployeeIds(scope.UserId);
  327. var closedQ = _db.Queryable<AdoS8Exception>()
  328. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
  329. .Where(x => x.Status == "CLOSED" || x.Status == "REJECTED" || x.Status == "RECOVERED")
  330. .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0));
  331. var closedCount = recIds.Count == 0 ? 0 : await closedQ.Clone().CountAsync();
  332. var closed = recIds.Count == 0
  333. ? new List<AdoS8Exception>()
  334. : await closedQ.Clone().OrderByDescending(x => x.ClosedAt ?? x.UpdatedAt).Take(top).ToListAsync();
  335. var donePage = await _flowTasks.MyDonePage(new TaskPageInput { Page = 1, PageSize = top });
  336. var doneCount = donePage.Total;
  337. var done = donePage.Items ?? new List<TaskPageOutput>();
  338. var items = actions.Select(x => new AidopWorkbenchItemDto
  339. {
  340. Id = x.Id.ToString(),
  341. Source = "SmartOpsActionCompleted",
  342. Category = "改善行动",
  343. Title = x.Content,
  344. Status = x.Status,
  345. DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
  346. PlanId = x.PlanId.ToString(),
  347. CompletedAt = (x.CompletedAt ?? x.UpdateTime).ToString("yyyy-MM-dd HH:mm"),
  348. JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.PlanId}&actionId={x.Id}",
  349. })
  350. .Concat(closed.Select(x => new AidopWorkbenchItemDto
  351. {
  352. Id = x.Id.ToString(),
  353. Source = "S8Completed",
  354. Category = "异常",
  355. Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
  356. Status = x.Status,
  357. Extra = x.ExceptionCode,
  358. CompletedAt = (x.ClosedAt ?? x.UpdatedAt ?? x.CreatedAt).ToString("yyyy-MM-dd HH:mm"),
  359. JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
  360. }))
  361. .Concat(done.Select(x => new AidopWorkbenchItemDto
  362. {
  363. Id = x.Id.ToString(),
  364. Source = "ApprovalCompleted",
  365. Category = "审批",
  366. Title = string.IsNullOrWhiteSpace(x.Title) ? x.BizType : x.Title,
  367. Status = x.Status.ToString(),
  368. Extra = x.BizType,
  369. CompletedAt = x.ActionTime?.ToString("yyyy-MM-dd HH:mm"),
  370. JumpUrl = ApprovalJumpUrl(x),
  371. }))
  372. .OrderByDescending(x => ParseDate(x.CompletedAt) ?? DateTime.MinValue)
  373. .Take(top)
  374. .ToList();
  375. counts.CompletedTotal = actionCount + closedCount + doneCount;
  376. return new AidopWorkbenchSectionDto { Total = counts.CompletedTotal, Items = items };
  377. }
  378. private async Task<List<long>> ResolveEmployeeIds(long userId)
  379. {
  380. return await _db.Queryable<AdoS0EmployeeMaster>().ClearFilter()
  381. .Where(x => x.SysUserId == userId)
  382. .Select(x => x.Id)
  383. .ToListAsync();
  384. }
  385. private static DateTime? ParseDate(string? value) =>
  386. DateTime.TryParse(value, out var parsed) ? parsed : null;
  387. private static string ApprovalJumpUrl(TaskPageOutput t)
  388. {
  389. var bizType = t.BizType ?? "";
  390. if (bizType.StartsWith("EXCEPTION", StringComparison.OrdinalIgnoreCase) && t.BizId > 0)
  391. return $"/aidop/s8/exceptions/{t.BizId}";
  392. if (string.Equals(bizType, "SMART_OPS_IMPROVEMENT", StringComparison.OrdinalIgnoreCase) && t.BizId > 0)
  393. return $"/aidop/smart-ops/improvement-plans?id={t.BizId}";
  394. return $"/aidop/flowManage/approvalFlowCenter?taskId={t.Id}";
  395. }
  396. }