namespace Admin.NET.Plugin.AiDOP.SmartOps; public sealed class S1KpiDailySnapshotRow { public string MetricCode { get; set; } = string.Empty; public DateTime BizDate { get; set; } public decimal? MetricValue { get; set; } } public interface IS1KpiDailySnapshotStore { Task ExecuteInTransactionAsync(Func action, CancellationToken ct = default); Task GetMaxBizDateAsync(int level, long tenantId, long factoryId, string moduleCode, CancellationToken ct = default); Task> ListLatestAsync(int level, long tenantId, long factoryId, DateTime bizDate, string moduleCode, CancellationToken ct = default); Task UpdateTargetsAsync( int level, long tenantId, long factoryId, DateTime bizDate, string metricCode, KpiTargetSnapshot snap, string moduleCode, CancellationToken ct = default); } public sealed class S1KpiTargetSnapshotLevelResult { public int Level { get; set; } public string? BizDate { get; set; } public int UpdatedRows { get; set; } public int MissingRows { get; set; } } public sealed class S1KpiTargetSnapshotRefreshResult { public bool Ok { get; set; } = true; public string ModuleCode { get; set; } = "S1"; public long FactoryId { get; set; } public int UpdatedRows { get; set; } public int MissingRows { get; set; } public List Levels { get; set; } = new(); public int ElapsedMs { get; set; } public string Message { get; set; } = string.Empty; } public sealed class S1KpiTargetSnapshotRefreshService : ITransient { public const string ModuleCode = "S1"; private readonly IS1KpiDailySnapshotStore _store; private readonly IKpiTargetResolver _resolver; public S1KpiTargetSnapshotRefreshService(IS1KpiDailySnapshotStore store, IKpiTargetResolver resolver) { _store = store; _resolver = resolver; } public async Task RefreshAsync(long tenantId, long factoryId, CancellationToken ct = default) => await RefreshAsync(ModuleCode, tenantId, factoryId, ct); public async Task RefreshAsync(string moduleCode, long tenantId, long factoryId, CancellationToken ct = default) { var mc = string.IsNullOrWhiteSpace(moduleCode) ? ModuleCode : moduleCode.Trim().ToUpperInvariant(); if (factoryId <= 0) throw new ArgumentOutOfRangeException(nameof(factoryId), "工厂必须大于 0"); if (tenantId <= 0) throw new ArgumentOutOfRangeException(nameof(tenantId), "租户无效"); var started = DateTime.Now; var result = new S1KpiTargetSnapshotRefreshResult { FactoryId = factoryId, ModuleCode = mc }; await _store.ExecuteInTransactionAsync(async () => { for (var level = 1; level <= 4; level++) { ct.ThrowIfCancellationRequested(); var levelResult = await RefreshLevelAsync(mc, level, tenantId, factoryId, ct); result.Levels.Add(levelResult); result.UpdatedRows += levelResult.UpdatedRows; result.MissingRows += levelResult.MissingRows; } }, ct); result.ElapsedMs = (int)(DateTime.Now - started).TotalMilliseconds; result.Message = result.UpdatedRows == 0 && result.Levels.All(x => x.BizDate == null) ? $"{mc} 暂无可刷新的日值快照" : $"{mc} 看板目标快照已刷新"; return result; } private async Task RefreshLevelAsync(string moduleCode, int level, long tenantId, long factoryId, CancellationToken ct) { var maxDate = await _store.GetMaxBizDateAsync(level, tenantId, factoryId, moduleCode, ct); var levelResult = new S1KpiTargetSnapshotLevelResult { Level = level }; if (maxDate == null) return levelResult; var bizDate = maxDate.Value.Date; levelResult.BizDate = bizDate.ToString("yyyy-MM-dd"); var rows = await _store.ListLatestAsync(level, tenantId, factoryId, bizDate, moduleCode, ct); foreach (var group in rows.GroupBy(x => x.MetricCode, StringComparer.OrdinalIgnoreCase)) { ct.ThrowIfCancellationRequested(); var snap = await _resolver.ResolveAsync(tenantId, factoryId, group.Key, moduleCode, bizDate, ct); if (snap.TargetSource == KpiTargetSnapshotSource.Missing || snap.TargetValue is null or <= 0) { levelResult.MissingRows += group.Count(); continue; } var updated = await _store.UpdateTargetsAsync(level, tenantId, factoryId, bizDate, group.Key, snap, moduleCode, ct); levelResult.UpdatedRows += updated; } return levelResult; } }