using Admin.NET.Plugin.AiDOP.Dto.SmartOps; namespace Admin.NET.Plugin.AiDOP.SmartOps; public sealed class KpiTargetMigrationPreviewDto { public List WillCreate { get; set; } = new(); public List Skipped { get; set; } = new(); public List Conflicts { get; set; } = new(); public List Missing { get; set; } = new(); public List DemoSuspect { get; set; } = new(); } public sealed class KpiTargetMigrationService : ITransient { private readonly ISqlSugarClient _db; private readonly IKpiTargetConfigStore _store; private readonly KpiTargetService _targets; private readonly UserManager _userManager; public KpiTargetMigrationService( ISqlSugarClient db, IKpiTargetConfigStore store, KpiTargetService targets, UserManager userManager) { _db = db; _store = store; _targets = targets; _userManager = userManager; } public async Task PreviewAsync(CancellationToken ct = default) { var tenantId = AidopTenantScope.ResolveOrThrow(_userManager); return await BuildAsync(tenantId, commit: false, ct); } public async Task CommitAsync(CancellationToken ct = default) { var tenantId = AidopTenantScope.ResolveOrThrow(_userManager); return await BuildAsync(tenantId, commit: true, ct); } private async Task BuildAsync(long tenantId, bool commit, CancellationToken ct) { var result = new KpiTargetMigrationPreviewDto(); var rows = await _db.Ado.SqlQueryAsync( """ SELECT factory_id AS FactoryId, metric_code AS MetricCode, module_code AS ModuleCode, target_value AS TargetValue, biz_date AS BizDate, target_source AS TargetSource FROM ado_s9_kpi_value_l1_day WHERE tenant_id=@TenantId AND is_deleted=0 AND target_value IS NOT NULL AND target_value>0 ORDER BY metric_code, factory_id, biz_date DESC, id DESC """, new SugarParameter("@TenantId", tenantId)); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var row in rows) { var key = $"{row.FactoryId}|{row.MetricCode}"; if (!seen.Add(key)) continue; if (string.Equals(row.TargetSource, KpiTargetSnapshotSource.DemoImport, StringComparison.OrdinalIgnoreCase)) { result.DemoSuspect.Add($"{row.MetricCode} factory={row.FactoryId}"); continue; } var existing = await _store.ListActiveScopeAsync(tenantId, row.FactoryId, row.MetricCode, ct); if (existing.Count > 0) { result.Skipped.Add($"{row.MetricCode} factory={row.FactoryId}"); continue; } var dto = new KpiTargetCreateDto { MetricCode = row.MetricCode, FactoryId = row.FactoryId, TargetValue = row.TargetValue, EffectiveFrom = DateTime.Today, ClosePrevious = false, Remark = $"LEGACY_MIGRATION from {row.BizDate:yyyy-MM-dd}" }; result.WillCreate.Add(dto); if (!commit) continue; try { await _targets.CreateCoreAsync(tenantId, dto, KpiTargetConfigSource.LegacyMigration, null, _userManager.UserId, ct); } catch (KpiTargetConflictException ex) { result.Conflicts.Add($"{row.MetricCode}: {ex.Message}"); } } var masters = await _db.Queryable() .ClearFilter() .Where(x => x.TenantId == tenantId && x.IsEnabled) .ToListAsync(ct); var createdKeys = result.WillCreate.Select(x => x.MetricCode).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var m in masters) { if (createdKeys.Contains(m.MetricCode) || seen.Contains($"1|{m.MetricCode}") || seen.Contains($"0|{m.MetricCode}")) continue; result.Missing.Add($"{m.ModuleCode}:{m.MetricCode}"); } return result; } private sealed class LegacyHit { public long FactoryId { get; set; } public string MetricCode { get; set; } = string.Empty; public string ModuleCode { get; set; } = string.Empty; public decimal TargetValue { get; set; } public DateTime BizDate { get; set; } public string? TargetSource { get; set; } } }