| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- using Admin.NET.Core;
- using Admin.NET.Plugin.AiDOP.Entity;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Admin.NET.Plugin.AiDOP.SmartOps;
- using Admin.NET.Plugin.AiDOP.Workbench.Dto;
- using Admin.NET.Plugin.ApprovalFlow;
- using Admin.NET.Plugin.ApprovalFlow.Service;
- using Microsoft.Extensions.Logging;
- namespace Admin.NET.Plugin.AiDOP.Workbench;
- public sealed class AidopWorkbenchService : ITransient
- {
- private readonly ISqlSugarClient _db;
- private readonly SmartOpsTrustedScopeResolver _trustedScope;
- private readonly FlowTaskService _flowTasks;
- private readonly ILogger<AidopWorkbenchService> _logger;
- public AidopWorkbenchService(
- ISqlSugarClient db,
- SmartOpsTrustedScopeResolver trustedScope,
- FlowTaskService flowTasks,
- ILogger<AidopWorkbenchService> logger)
- {
- _db = db;
- _trustedScope = trustedScope;
- _flowTasks = flowTasks;
- _logger = logger;
- }
- public async Task<AidopWorkbenchSummaryDto> GetSummaryAsync(int top)
- {
- var scope = await _trustedScope.ResolveAsync();
- top = Math.Clamp(top <= 0 ? 6 : top, 1, 20);
- var dto = new AidopWorkbenchSummaryDto
- {
- GeneratedAt = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:sszzz"),
- };
- dto.ImprovementActions = await SafeSection("improvement", () => LoadImprovementAsync(scope, top, dto.Counts));
- dto.ApprovalTasks = await SafeSection("approval", () => LoadApprovalsAsync(top, dto.Counts));
- dto.S8Tasks = await SafeSection("s8", () => LoadS8Async(scope, top, dto.Counts));
- dto.UnreadNotices = await SafeSection("notice", () => LoadNoticesAsync(scope, top, dto.Counts));
- dto.Pending = MergePending(dto, top);
- dto.Counts.PendingTotal = dto.Pending.Total;
- dto.Initiated = await SafeSection("initiated", () => LoadInitiatedAsync(scope, top, dto.Counts));
- dto.Completed = await SafeSection("completed", () => LoadCompletedAsync(scope, top, dto.Counts));
- return dto;
- }
- private async Task<AidopWorkbenchSectionDto> SafeSection(string name, Func<Task<AidopWorkbenchSectionDto>> factory)
- {
- try
- {
- return await factory();
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "工作台分区加载失败 section={Section}", name);
- return new AidopWorkbenchSectionDto
- {
- Available = false,
- ErrorCode = "SECTION_FAILED",
- Message = "该分区暂时不可用",
- };
- }
- }
- private async Task<AidopWorkbenchSectionDto> LoadImprovementAsync(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
- {
- var today = DateTime.Today;
- var q = _db.Queryable<AdoSmartOpsImprovementAction>()
- .Where(x => x.TenantId == scope.TenantId && x.OwnerUserId == scope.UserId)
- .Where(x => x.Status == SmartOpsImprovementActionStatus.Pending
- || x.Status == SmartOpsImprovementActionStatus.InProgress);
- var open = await q.Clone().CountAsync();
- var dueSoon = await q.Clone()
- .Where(x => x.DueDate != null && x.DueDate.Value.Date >= today && x.DueDate.Value.Date <= today.AddDays(1))
- .CountAsync();
- var overdue = await q.Clone()
- .Where(x => x.DueDate != null && x.DueDate.Value.Date < today)
- .CountAsync();
- counts.OverdueTotal += overdue;
- counts.DueToday += await q.Clone()
- .Where(x => x.DueDate != null && x.DueDate.Value >= today && x.DueDate.Value < today.AddDays(1))
- .CountAsync();
- counts.ImprovementOpen = open;
- counts.ImprovementDueSoon = dueSoon;
- counts.ImprovementOverdue = overdue;
- var rows = await q.Clone().OrderBy(x => x.DueDate).Take(top).ToListAsync();
- var planIds = rows.Select(x => x.PlanId).Distinct().ToList();
- var plans = planIds.Count == 0
- ? new List<AdoSmartOpsImprovementPlan>()
- : await _db.Queryable<AdoSmartOpsImprovementPlan>()
- .Where(x => x.TenantId == scope.TenantId && planIds.Contains(x.Id))
- .ToListAsync();
- var planMap = plans.ToDictionary(x => x.Id, x => x);
- return new AidopWorkbenchSectionDto
- {
- Total = open,
- Items = rows.Select(x =>
- {
- planMap.TryGetValue(x.PlanId, out var plan);
- return new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "SmartOpsAction",
- Title = x.Content,
- Status = SmartOpsImprovementActionStatus.DisplayStatus(x.Status, x.DueDate, today),
- DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
- Overdue = SmartOpsImprovementActionStatus.IsOverdue(x.Status, x.DueDate, today),
- PlanId = x.PlanId.ToString(),
- PlanNo = plan?.PlanNo,
- Category = "改善行动",
- CreatedAt = x.CreateTime.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.PlanId}&actionId={x.Id}",
- };
- }).ToList(),
- };
- }
- private async Task<AidopWorkbenchSectionDto> LoadApprovalsAsync(int top, AidopWorkbenchCountsDto counts)
- {
- counts.ApprovalPending = await _flowTasks.MyPendingCount();
- var page = await _flowTasks.MyPendingPage(new TaskPageInput { Page = 1, PageSize = top });
- return new AidopWorkbenchSectionDto
- {
- Total = counts.ApprovalPending,
- Items = (page.Items ?? new List<TaskPageOutput>()).Select(t => new AidopWorkbenchItemDto
- {
- Id = t.Id.ToString(),
- Source = "Approval",
- Title = string.IsNullOrWhiteSpace(t.Title) ? t.BizType : t.Title,
- Status = "pending",
- Extra = t.BizType,
- Category = "审批",
- CreatedAt = t.CreateTime.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = ApprovalJumpUrl(t),
- }).ToList(),
- };
- }
- private async Task<AidopWorkbenchSectionDto> LoadS8Async(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
- {
- var recIds = await _db.Queryable<AdoS0EmployeeMaster>().ClearFilter()
- .Where(x => x.SysUserId == scope.UserId)
- .Select(x => x.Id)
- .ToListAsync();
- if (recIds.Count == 0)
- {
- return new AidopWorkbenchSectionDto
- {
- Available = false,
- ErrorCode = "EMPLOYEE_UNBOUND",
- Message = "当前账号未绑定员工主数据,无法列出个人 S8 事项",
- };
- }
- var assignedQ = _db.Queryable<AdoS8Exception>()
- .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.AssigneeId ?? 0))
- .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED");
- var verifyQ = _db.Queryable<AdoS8Exception>()
- .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.VerifierId ?? 0))
- .Where(x => x.Status == "PENDING_VERIFICATION");
- counts.S8Assigned = await assignedQ.Clone().CountAsync();
- counts.S8Verification = await verifyQ.Clone().CountAsync();
- var today = DateTime.Today;
- counts.DueToday += await _db.Queryable<AdoS8Exception>()
- .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
- .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED")
- .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0))
- .Where(x => x.SlaDeadline != null && x.SlaDeadline >= today && x.SlaDeadline < today.AddDays(1))
- .CountAsync();
- counts.OverdueTotal += await _db.Queryable<AdoS8Exception>()
- .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
- .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED")
- .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0))
- .Where(x => x.TimeoutFlag || (x.SlaDeadline != null && x.SlaDeadline < DateTime.Now))
- .CountAsync();
- var assigned = await assignedQ.Clone().OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(top).ToListAsync();
- var verify = await verifyQ.Clone().OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(top).ToListAsync();
- var merged = assigned.Concat(verify)
- .GroupBy(x => x.Id)
- .Select(g => g.First())
- .Take(top)
- .ToList();
- return new AidopWorkbenchSectionDto
- {
- Total = counts.S8Assigned + counts.S8Verification,
- Items = merged.Select(x => new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "S8Exception",
- Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
- Status = x.Status,
- DueDate = x.SlaDeadline?.ToString("yyyy-MM-dd"),
- Overdue = x.TimeoutFlag || (x.SlaDeadline.HasValue && x.SlaDeadline < DateTime.Now && x.Status != "CLOSED"),
- Extra = x.ExceptionCode,
- Category = "异常",
- CreatedAt = x.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
- }).ToList(),
- };
- }
- private async Task<AidopWorkbenchSectionDto> LoadNoticesAsync(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
- {
- var q = _db.Queryable<SysNoticeUser>()
- .Includes(u => u.SysNotice)
- .Where(u => u.UserId == scope.UserId && u.ReadStatus == NoticeUserStatusEnum.UNREAD);
- counts.NoticeUnread = await q.Clone().CountAsync();
- var rows = await q.Clone()
- .OrderByDescending(u => u.SysNotice.CreateTime)
- .Take(top)
- .ToListAsync();
- return new AidopWorkbenchSectionDto
- {
- Total = counts.NoticeUnread,
- Items = rows.Where(x => x.SysNotice != null).Select(x => new AidopWorkbenchItemDto
- {
- Id = x.SysNotice.Id.ToString(),
- Source = "SysNotice",
- Title = x.SysNotice.Title,
- Status = "unread",
- Extra = x.SysNotice.PublicTime?.ToString("yyyy-MM-dd HH:mm"),
- Category = "消息",
- CreatedAt = x.SysNotice.CreateTime.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = $"/dashboard/notice?noticeId={x.SysNotice.Id}",
- }).ToList(),
- };
- }
- private static AidopWorkbenchSectionDto MergePending(AidopWorkbenchSummaryDto dto, int top)
- {
- var sections = new[] { dto.ImprovementActions, dto.ApprovalTasks, dto.S8Tasks };
- var items = sections.SelectMany(x => x.Items)
- .OrderByDescending(x => x.Overdue)
- .ThenBy(x => ParseDate(x.DueDate) ?? DateTime.MaxValue)
- .ThenByDescending(x => ParseDate(x.CreatedAt) ?? DateTime.MinValue)
- .Take(top)
- .ToList();
- return new AidopWorkbenchSectionDto
- {
- Available = sections.Any(x => x.Available),
- Total = sections.Sum(x => x.Total),
- Items = items,
- Message = sections.All(x => !x.Available) ? "待办数据暂时不可用" : null,
- };
- }
- private async Task<AidopWorkbenchSectionDto> LoadInitiatedAsync(
- SmartOpsTrustedScope scope,
- int top,
- AidopWorkbenchCountsDto counts)
- {
- var actionPlanIds = await _db.Queryable<AdoSmartOpsImprovementAction>()
- .Where(x => x.TenantId == scope.TenantId && x.CreateUserId == scope.UserId)
- .Select(x => x.PlanId)
- .Distinct()
- .ToListAsync();
- var planCount = actionPlanIds.Count;
- var plans = actionPlanIds.Count == 0
- ? new List<AdoSmartOpsImprovementPlan>()
- : await _db.Queryable<AdoSmartOpsImprovementPlan>()
- .Where(x => x.TenantId == scope.TenantId && actionPlanIds.Contains(x.Id))
- .OrderByDescending(x => x.CreateTime)
- .Take(top)
- .ToListAsync();
- var recIds = await ResolveEmployeeIds(scope.UserId);
- var reportedQ = _db.Queryable<AdoS8Exception>()
- .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.ReporterId ?? 0));
- var reportedCount = recIds.Count == 0 ? 0 : await reportedQ.Clone().CountAsync();
- var reported = recIds.Count == 0
- ? new List<AdoS8Exception>()
- : await reportedQ.Clone().OrderByDescending(x => x.CreatedAt).Take(top).ToListAsync();
- var flowQ = _db.Queryable<ApprovalFlowInstance>()
- .Where(x => x.InitiatorId == scope.UserId)
- .Where(x => x.BizType != "SMART_OPS_IMPROVEMENT" && !x.BizType.StartsWith("EXCEPTION_"));
- var flowCount = await flowQ.Clone().CountAsync();
- var flows = await flowQ.Clone().OrderByDescending(x => x.StartTime).Take(top).ToListAsync();
- var items = plans.Select(x => new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "ImprovementPlan",
- Category = "改善计划",
- Title = string.IsNullOrWhiteSpace(x.ProblemName) ? x.PlanNo : x.ProblemName,
- Status = x.Status,
- DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
- PlanId = x.Id.ToString(),
- PlanNo = x.PlanNo,
- CreatedAt = x.CreateTime.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.Id}",
- })
- .Concat(reported.Select(x => new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "S8Reported",
- Category = "异常提报",
- Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
- Status = x.Status,
- DueDate = x.SlaDeadline?.ToString("yyyy-MM-dd"),
- Extra = x.ExceptionCode,
- CreatedAt = x.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
- }))
- .Concat(flows.Select(x => new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "ApprovalInitiated",
- Category = "审批发起",
- Title = x.Title,
- Status = x.Status.ToString(),
- Extra = x.BizType,
- CreatedAt = x.StartTime.ToString("yyyy-MM-dd HH:mm"),
- CompletedAt = x.EndTime?.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = "/aidop/flowManage/approvalFlowCenter",
- }))
- .OrderByDescending(x => ParseDate(x.CreatedAt) ?? DateTime.MinValue)
- .Take(top)
- .ToList();
- counts.InitiatedTotal = planCount + reportedCount + flowCount;
- return new AidopWorkbenchSectionDto { Total = counts.InitiatedTotal, Items = items };
- }
- private async Task<AidopWorkbenchSectionDto> LoadCompletedAsync(
- SmartOpsTrustedScope scope,
- int top,
- AidopWorkbenchCountsDto counts)
- {
- var actionsQ = _db.Queryable<AdoSmartOpsImprovementAction>()
- .Where(x => x.TenantId == scope.TenantId && x.OwnerUserId == scope.UserId)
- .Where(x => x.Status == SmartOpsImprovementActionStatus.Completed);
- var actionCount = await actionsQ.Clone().CountAsync();
- var actions = await actionsQ.Clone()
- .OrderByDescending(x => x.CompletedAt ?? x.UpdateTime)
- .Take(top)
- .ToListAsync();
- var recIds = await ResolveEmployeeIds(scope.UserId);
- var closedQ = _db.Queryable<AdoS8Exception>()
- .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
- .Where(x => x.Status == "CLOSED" || x.Status == "REJECTED" || x.Status == "RECOVERED")
- .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0));
- var closedCount = recIds.Count == 0 ? 0 : await closedQ.Clone().CountAsync();
- var closed = recIds.Count == 0
- ? new List<AdoS8Exception>()
- : await closedQ.Clone().OrderByDescending(x => x.ClosedAt ?? x.UpdatedAt).Take(top).ToListAsync();
- var donePage = await _flowTasks.MyDonePage(new TaskPageInput { Page = 1, PageSize = top });
- var doneCount = donePage.Total;
- var done = donePage.Items ?? new List<TaskPageOutput>();
- var items = actions.Select(x => new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "SmartOpsActionCompleted",
- Category = "改善行动",
- Title = x.Content,
- Status = x.Status,
- DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
- PlanId = x.PlanId.ToString(),
- CompletedAt = (x.CompletedAt ?? x.UpdateTime).ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.PlanId}&actionId={x.Id}",
- })
- .Concat(closed.Select(x => new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "S8Completed",
- Category = "异常",
- Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
- Status = x.Status,
- Extra = x.ExceptionCode,
- CompletedAt = (x.ClosedAt ?? x.UpdatedAt ?? x.CreatedAt).ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
- }))
- .Concat(done.Select(x => new AidopWorkbenchItemDto
- {
- Id = x.Id.ToString(),
- Source = "ApprovalCompleted",
- Category = "审批",
- Title = string.IsNullOrWhiteSpace(x.Title) ? x.BizType : x.Title,
- Status = x.Status.ToString(),
- Extra = x.BizType,
- CompletedAt = x.ActionTime?.ToString("yyyy-MM-dd HH:mm"),
- JumpUrl = ApprovalJumpUrl(x),
- }))
- .OrderByDescending(x => ParseDate(x.CompletedAt) ?? DateTime.MinValue)
- .Take(top)
- .ToList();
- counts.CompletedTotal = actionCount + closedCount + doneCount;
- return new AidopWorkbenchSectionDto { Total = counts.CompletedTotal, Items = items };
- }
- private async Task<List<long>> ResolveEmployeeIds(long userId)
- {
- return await _db.Queryable<AdoS0EmployeeMaster>().ClearFilter()
- .Where(x => x.SysUserId == userId)
- .Select(x => x.Id)
- .ToListAsync();
- }
- private static DateTime? ParseDate(string? value) =>
- DateTime.TryParse(value, out var parsed) ? parsed : null;
- private static string ApprovalJumpUrl(TaskPageOutput t)
- {
- var bizType = t.BizType ?? "";
- if (bizType.StartsWith("EXCEPTION", StringComparison.OrdinalIgnoreCase) && t.BizId > 0)
- return $"/aidop/s8/exceptions/{t.BizId}";
- if (string.Equals(bizType, "SMART_OPS_IMPROVEMENT", StringComparison.OrdinalIgnoreCase) && t.BizId > 0)
- return $"/aidop/smart-ops/improvement-plans?id={t.BizId}";
- return $"/aidop/flowManage/approvalFlowCenter?taskId={t.Id}";
- }
- }
|