| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- using System.Text.Json;
- using Admin.NET.Core;
- using Admin.NET.Plugin.AiDOP.Controllers;
- using Admin.NET.Plugin.AiDOP.Entity;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- namespace Admin.NET.Plugin.AiDOP.SmartOps;
- public sealed class SmartOpsImprovementActionService : ITransient
- {
- private static readonly JsonSerializerOptions JsonOptions = new()
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- PropertyNameCaseInsensitive = true,
- };
- private readonly ISqlSugarClient _db;
- public SmartOpsImprovementActionService(ISqlSugarClient db) => _db = db;
- public async Task<List<AdoSmartOpsImprovementAction>> ListByPlanAsync(long tenantId, long planId)
- {
- await MigrateLegacyJsonAsync(tenantId, planId);
- return await _db.Queryable<AdoSmartOpsImprovementAction>()
- .Where(x => x.TenantId == tenantId && x.PlanId == planId)
- .OrderBy(x => x.SequenceNo)
- .ToListAsync();
- }
- public async Task<List<AdoSmartOpsImprovementAction>> ListMineAsync(SmartOpsTrustedScope scope)
- {
- return await _db.Queryable<AdoSmartOpsImprovementAction>()
- .Where(x => x.TenantId == scope.TenantId && x.OwnerUserId == scope.UserId)
- .OrderBy(x => x.DueDate)
- .ToListAsync();
- }
- public async Task ReplaceFromDraftAsync(
- AdoSmartOpsImprovementPlan plan,
- IReadOnlyList<ImprovementActionItem> drafts,
- SmartOpsTrustedScope scope)
- {
- var existing = await _db.Queryable<AdoSmartOpsImprovementAction>()
- .Where(x => x.TenantId == plan.TenantId && x.PlanId == plan.Id)
- .ToListAsync();
- if (existing.Count > 0)
- return;
- var now = DateTime.Now;
- var rows = new List<AdoSmartOpsImprovementAction>();
- for (var i = 0; i < drafts.Count; i++)
- {
- var draft = drafts[i];
- if (string.IsNullOrWhiteSpace(draft.Content)) continue;
- var ownerUserId = ParseOwnerUserId(draft.Owner);
- rows.Add(new AdoSmartOpsImprovementAction
- {
- TenantId = plan.TenantId,
- FactoryId = plan.FactoryId,
- PlanId = plan.Id,
- SequenceNo = rows.Count + 1,
- Content = draft.Content.Trim(),
- OwnerUserId = ownerUserId,
- OwnerName = draft.Owner,
- DueDate = ParseDate(draft.DueDate),
- Status = SmartOpsImprovementActionStatus.Normalize(draft.Status),
- CompletedAt = ParseDate(draft.CompletedAt),
- Version = 1,
- CreateUserId = scope.UserId,
- UpdateUserId = scope.UserId,
- CreateTime = now,
- UpdateTime = now,
- });
- }
- if (rows.Count == 0) return;
- await _db.Insertable(rows).ExecuteCommandAsync();
- foreach (var row in rows)
- {
- await WriteLogAsync(row, "create", null, row, scope.UserId, "从诊断创建");
- if (row.OwnerUserId is > 0)
- await NotifyAsync(plan, row, SmartOpsImprovementNoticeType.Assigned, scope.UserId);
- }
- }
- public async Task<AdoSmartOpsImprovementAction> MutateAsync(
- long planId,
- int? sequenceNo,
- long? actionId,
- string? status,
- string? completedAt,
- long? ownerUserId,
- string? ownerName,
- string? proofRemark,
- SmartOpsTrustedScope scope)
- {
- var plan = await _db.Queryable<AdoSmartOpsImprovementPlan>()
- .FirstAsync(x => x.Id == planId && x.TenantId == scope.TenantId)
- ?? throw Oops.Oh("改善计划不存在");
- await MigrateLegacyJsonAsync(scope.TenantId, plan.Id);
- AdoSmartOpsImprovementAction? action = null;
- if (actionId is > 0)
- {
- action = await _db.Queryable<AdoSmartOpsImprovementAction>()
- .FirstAsync(x => x.Id == actionId && x.TenantId == scope.TenantId && x.PlanId == plan.Id);
- }
- else if (sequenceNo is > 0)
- {
- action = await _db.Queryable<AdoSmartOpsImprovementAction>()
- .FirstAsync(x => x.TenantId == scope.TenantId && x.PlanId == plan.Id && x.SequenceNo == sequenceNo);
- }
- if (action == null) throw Oops.Oh("行动项不存在");
- EnsureCanMutate(plan, action, scope);
- var before = Snapshot(action);
- var previousOwnerUserId = action.OwnerUserId;
- var nextStatus = string.IsNullOrWhiteSpace(status)
- ? action.Status
- : SmartOpsImprovementActionStatus.Normalize(status);
- if (!SmartOpsImprovementActionStatus.CanTransition(action.Status, nextStatus))
- throw Oops.Oh($"行动项不能从 {action.Status} 变更为 {nextStatus}");
- if (ownerUserId.HasValue) action.OwnerUserId = ownerUserId;
- if (ownerName != null) action.OwnerName = ownerName;
- if (proofRemark != null) action.ProofRemark = proofRemark;
- var statusChanged = !string.Equals(action.Status, nextStatus, StringComparison.OrdinalIgnoreCase);
- var assigned = ownerUserId is > 0 && ownerUserId != previousOwnerUserId;
- action.Status = nextStatus;
- action.CompletedAt = nextStatus == SmartOpsImprovementActionStatus.Completed
- ? ParseDate(completedAt) ?? DateTime.Today
- : null;
- action.Version += 1;
- action.UpdateUserId = scope.UserId;
- action.UpdateTime = DateTime.Now;
- await _db.Updateable(action)
- .Where(x => x.Id == action.Id && x.TenantId == scope.TenantId && x.Version == action.Version - 1)
- .ExecuteCommandAsync();
- await WriteLogAsync(action, statusChanged ? "status" : "update", before, action, scope.UserId, proofRemark);
- if (assigned)
- await NotifyAsync(plan, action, SmartOpsImprovementNoticeType.Assigned, scope.UserId);
- else if (statusChanged)
- await NotifyAsync(plan, action, SmartOpsImprovementNoticeType.StatusChanged, scope.UserId);
- return action;
- }
- public async Task<List<AdoSmartOpsImprovementActionLog>> ListLogsAsync(long tenantId, long actionId)
- {
- return await _db.Queryable<AdoSmartOpsImprovementActionLog>()
- .Where(x => x.TenantId == tenantId && x.ActionId == actionId)
- .OrderByDescending(x => x.CreateTime)
- .ToListAsync();
- }
- public async Task MigrateLegacyJsonAsync(long tenantId, long? planId = null)
- {
- var q = _db.Queryable<AdoSmartOpsImprovementPlan>()
- .Where(x => x.TenantId == tenantId && !string.IsNullOrWhiteSpace(x.ActionItemsJson));
- if (planId is > 0) q = q.Where(x => x.Id == planId);
- var plans = await q.ToListAsync();
- foreach (var plan in plans)
- {
- var exists = await _db.Queryable<AdoSmartOpsImprovementAction>()
- .AnyAsync(x => x.TenantId == tenantId && x.PlanId == plan.Id);
- if (exists) continue;
- var drafts = ParseDrafts(plan.ActionItemsJson);
- if (drafts.Count == 0) continue;
- await ReplaceFromDraftAsync(plan, drafts, new SmartOpsTrustedScope(tenantId, plan.FactoryId, plan.OwnerUserId ?? 0));
- }
- }
- public static List<ImprovementActionItem> ToLegacyItems(IEnumerable<AdoSmartOpsImprovementAction> actions)
- {
- return actions.Select(x => new ImprovementActionItem
- {
- Content = x.Content,
- Owner = x.OwnerName,
- DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
- Status = SmartOpsImprovementActionStatus.DisplayStatus(x.Status, x.DueDate, DateTime.Today),
- CompletedAt = x.CompletedAt?.ToString("yyyy-MM-dd"),
- }).ToList();
- }
- public static object ToApiItem(AdoSmartOpsImprovementAction x) => new
- {
- x.Id,
- x.SequenceNo,
- x.Content,
- Owner = x.OwnerName,
- x.OwnerUserId,
- DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
- Status = SmartOpsImprovementActionStatus.DisplayStatus(x.Status, x.DueDate, DateTime.Today),
- CompletedAt = x.CompletedAt?.ToString("yyyy-MM-dd"),
- x.ProofRemark,
- Overdue = SmartOpsImprovementActionStatus.IsOverdue(x.Status, x.DueDate, DateTime.Today),
- };
- internal static List<ImprovementActionItem> ParseDrafts(string? json)
- {
- if (string.IsNullOrWhiteSpace(json)) return new List<ImprovementActionItem>();
- try
- {
- return JsonSerializer.Deserialize<List<ImprovementActionItem>>(json, JsonOptions)
- ?? new List<ImprovementActionItem>();
- }
- catch
- {
- return new List<ImprovementActionItem>();
- }
- }
- private static void EnsureCanMutate(
- AdoSmartOpsImprovementPlan plan,
- AdoSmartOpsImprovementAction action,
- SmartOpsTrustedScope scope)
- {
- if (scope.UserId > 0 && scope.UserId == action.OwnerUserId) return;
- if (scope.UserId > 0 && scope.UserId == action.CreateUserId) return;
- if (scope.UserId > 0 && plan.OwnerUserId is > 0 && scope.UserId == plan.OwnerUserId) return;
- throw Oops.Oh("无权修改该行动项");
- }
- private async Task WriteLogAsync(
- AdoSmartOpsImprovementAction action,
- string operation,
- object? before,
- object after,
- long operatorUserId,
- string? remark)
- {
- await _db.Insertable(new AdoSmartOpsImprovementActionLog
- {
- TenantId = action.TenantId,
- ActionId = action.Id,
- PlanId = action.PlanId,
- Operation = operation,
- BeforeJson = before == null ? null : JsonSerializer.Serialize(before, JsonOptions),
- AfterJson = JsonSerializer.Serialize(Snapshot(action), JsonOptions),
- OperatorUserId = operatorUserId,
- Remark = remark,
- CreateTime = DateTime.Now,
- }).ExecuteCommandAsync();
- }
- private async Task NotifyAsync(
- AdoSmartOpsImprovementPlan plan,
- AdoSmartOpsImprovementAction action,
- string noticeType,
- long operatorUserId)
- {
- if (action.OwnerUserId is not > 0) return;
- var today = DateTime.Today;
- var claimed = await TryClaimNoticeAsync(action.TenantId ?? 0, action.Id, noticeType, today);
- if (!claimed) return;
- var title = noticeType switch
- {
- SmartOpsImprovementNoticeType.Assigned => "改善任务已分派",
- SmartOpsImprovementNoticeType.DueSoon => "改善任务即将到期",
- SmartOpsImprovementNoticeType.Overdue => "改善任务已逾期",
- _ => "改善任务状态变更",
- };
- var due = action.DueDate?.ToString("yyyy-MM-dd") ?? "-";
- var content =
- $"计划 {plan.PlanNo} 行动项#{action.SequenceNo}:{action.Content}。到期日 {due}。详情 /aidop/smart-diagnosis";
- var now = DateTime.Now;
- var notice = new SysNotice
- {
- Title = title,
- Content = content,
- Type = NoticeTypeEnum.NOTICE,
- PublicUserId = operatorUserId > 0 ? operatorUserId : action.OwnerUserId.Value,
- PublicUserName = "智慧诊断",
- PublicTime = now,
- Status = NoticeStatusEnum.PUBLIC,
- CreateTime = now,
- CreateUserId = operatorUserId > 0 ? operatorUserId : action.OwnerUserId,
- CreateUserName = "智慧诊断",
- };
- var noticeId = await _db.Insertable(notice).ExecuteReturnSnowflakeIdAsync();
- await _db.Insertable(new SysNoticeUser
- {
- NoticeId = noticeId,
- UserId = action.OwnerUserId.Value,
- ReadStatus = NoticeUserStatusEnum.UNREAD,
- }).ExecuteCommandAsync();
- await _db.Updateable<AdoSmartOpsImprovementActionNotice>()
- .SetColumns(x => x.NoticeId == noticeId)
- .Where(x => x.TenantId == action.TenantId && x.ActionId == action.Id
- && x.NoticeType == noticeType && x.NoticeDate == today)
- .ExecuteCommandAsync();
- }
- public async Task<int> SendDueRemindersAsync(ISqlSugarClient db, long tenantId, DateTime today)
- {
- var actions = await db.Queryable<AdoSmartOpsImprovementAction>()
- .Where(x => x.TenantId == tenantId && x.OwnerUserId > 0 && x.DueDate != null)
- .Where(x => x.Status == SmartOpsImprovementActionStatus.Pending
- || x.Status == SmartOpsImprovementActionStatus.InProgress)
- .ToListAsync();
- var sent = 0;
- foreach (var action in actions)
- {
- var plan = await db.Queryable<AdoSmartOpsImprovementPlan>()
- .FirstAsync(x => x.Id == action.PlanId && x.TenantId == tenantId);
- if (plan == null) continue;
- var due = action.DueDate!.Value.Date;
- string? type = null;
- if (due < today) type = SmartOpsImprovementNoticeType.Overdue;
- else if (due <= today.AddDays(1)) type = SmartOpsImprovementNoticeType.DueSoon;
- if (type == null) continue;
- try
- {
- var scoped = new SmartOpsImprovementActionService(db);
- await scoped.NotifyAsync(plan, action, type, action.OwnerUserId ?? 0);
- sent++;
- }
- catch
- {
- // Keep scanning remaining tenants/actions.
- }
- }
- return sent;
- }
- private async Task<bool> TryClaimNoticeAsync(long tenantId, long actionId, string noticeType, DateTime date)
- {
- try
- {
- await _db.Insertable(new AdoSmartOpsImprovementActionNotice
- {
- TenantId = tenantId,
- ActionId = actionId,
- NoticeType = noticeType,
- NoticeDate = date.Date,
- CreateTime = DateTime.Now,
- }).ExecuteCommandAsync();
- return true;
- }
- catch (Exception ex) when (ex.Message.Contains("Duplicate", StringComparison.OrdinalIgnoreCase)
- || ex.InnerException?.Message.Contains("Duplicate", StringComparison.OrdinalIgnoreCase) == true)
- {
- return false;
- }
- }
- private static object Snapshot(AdoSmartOpsImprovementAction x) => new
- {
- x.Id,
- x.SequenceNo,
- x.Content,
- Owner = x.OwnerName,
- x.OwnerUserId,
- x.Status,
- DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
- CompletedAt = x.CompletedAt?.ToString("yyyy-MM-dd"),
- x.Version,
- };
- private static DateTime? ParseDate(string? raw) =>
- DateTime.TryParse(raw, out var value) ? value.Date : null;
- private static long? ParseOwnerUserId(string? raw) =>
- long.TryParse(raw, out var value) && value > 0 ? value : null;
- }
|