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> ListByPlanAsync(long tenantId, long planId) { await MigrateLegacyJsonAsync(tenantId, planId); return await _db.Queryable() .Where(x => x.TenantId == tenantId && x.PlanId == planId) .OrderBy(x => x.SequenceNo) .ToListAsync(); } public async Task> ListMineAsync(SmartOpsTrustedScope scope) { return await _db.Queryable() .Where(x => x.TenantId == scope.TenantId && x.OwnerUserId == scope.UserId) .OrderBy(x => x.DueDate) .ToListAsync(); } public async Task ReplaceFromDraftAsync( AdoSmartOpsImprovementPlan plan, IReadOnlyList drafts, SmartOpsTrustedScope scope) { var existing = await _db.Queryable() .Where(x => x.TenantId == plan.TenantId && x.PlanId == plan.Id) .ToListAsync(); if (existing.Count > 0) return; var now = DateTime.Now; var rows = new List(); 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 MutateAsync( long planId, int? sequenceNo, long? actionId, string? status, string? completedAt, long? ownerUserId, string? ownerName, string? proofRemark, SmartOpsTrustedScope scope) { var plan = await _db.Queryable() .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() .FirstAsync(x => x.Id == actionId && x.TenantId == scope.TenantId && x.PlanId == plan.Id); } else if (sequenceNo is > 0) { action = await _db.Queryable() .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> ListLogsAsync(long tenantId, long actionId) { return await _db.Queryable() .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() .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() .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 ToLegacyItems(IEnumerable 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 ParseDrafts(string? json) { if (string.IsNullOrWhiteSpace(json)) return new List(); try { return JsonSerializer.Deserialize>(json, JsonOptions) ?? new List(); } catch { return new List(); } } 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() .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 SendDueRemindersAsync(ISqlSugarClient db, long tenantId, DateTime today) { var actions = await db.Queryable() .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() .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 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; }