| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- using System.Text.Json;
- using Admin.NET.Plugin.AiDOP.Dto.SmartOps;
- using Admin.NET.Plugin.AiDOP.Entity;
- namespace Admin.NET.Plugin.AiDOP.SmartOps;
- public sealed class KpiTargetService : ITransient
- {
- public const int MaxImportRows = 10000;
- public const long MaxImportBytes = 5 * 1024 * 1024;
- private readonly IKpiTargetConfigStore _store;
- private readonly IKpiTargetResolver _resolver;
- private readonly UserManager _userManager;
- public KpiTargetService(IKpiTargetConfigStore store, IKpiTargetResolver resolver, UserManager userManager)
- {
- _store = store;
- _resolver = resolver;
- _userManager = userManager;
- }
- public long TenantId => AidopTenantScope.ResolveOrThrow(_userManager);
- public async Task<List<KpiTargetDto>> ListAsync(string metricCode, long? factoryId, CancellationToken ct = default)
- {
- EnsureMetricCode(metricCode);
- var tenantId = TenantId;
- await EnsureKpiAsync(tenantId, metricCode, ct);
- var list = await _store.ListAsync(tenantId, metricCode.Trim(), factoryId, ct);
- return list.Select(KpiTargetRules.ToDto).ToList();
- }
- public async Task<KpiTargetResolveResult> ResolveCurrentAsync(string metricCode, long factoryId, DateTime? bizDate, CancellationToken ct = default)
- {
- EnsureMetricCode(metricCode);
- if (factoryId < 0)
- throw new KpiTargetValidationException("工厂无效");
- var tenantId = TenantId;
- var kpi = await EnsureKpiAsync(tenantId, metricCode, ct);
- var day = (bizDate ?? DateTime.Today).Date;
- var snap = await _resolver.ResolveAsync(tenantId, factoryId, metricCode.Trim(), kpi.ModuleCode, day, ct);
- return new KpiTargetResolveResult
- {
- MetricCode = metricCode.Trim(),
- FactoryId = factoryId,
- BizDate = day.ToString("yyyy-MM-dd"),
- TargetValue = snap.TargetValue,
- TargetConfigId = snap.TargetConfigId,
- TargetSource = snap.TargetSource == KpiTargetSnapshotSource.Missing
- ? KpiTargetSnapshotSource.Missing
- : snap.TargetSource,
- ConfigSourceType = snap.TargetConfigId == null ? null : await ConfigSourceAsync(tenantId, snap.TargetConfigId.Value, ct),
- EffectiveFrom = snap.EffectiveFrom?.ToString("yyyy-MM-dd"),
- EffectiveTo = snap.EffectiveTo?.ToString("yyyy-MM-dd"),
- ResolvedAt = snap.ResolvedAt
- };
- }
- public Task<KpiTargetDto> CreateAsync(KpiTargetCreateDto dto, string sourceType, string? batchId, CancellationToken ct = default) =>
- CreateCoreAsync(TenantId, dto, sourceType, batchId, _userManager.UserId, ct);
- public async Task<KpiTargetDto> CreateCoreAsync(
- long tenantId,
- KpiTargetCreateDto dto,
- string sourceType,
- string? batchId,
- long? userId,
- CancellationToken ct = default)
- {
- EnsureMetricCode(dto.MetricCode);
- if (dto.FactoryId < 0)
- throw new KpiTargetValidationException("工厂无效");
- KpiTargetRules.EnsureTargetPositive(dto.TargetValue);
- var from = KpiTargetRules.Normalize(dto.EffectiveFrom);
- KpiTargetRules.EnsureRange(from, dto.EffectiveTo);
- await EnsureKpiAsync(tenantId, dto.MetricCode, ct);
- var existing = await _store.ListActiveScopeAsync(tenantId, dto.FactoryId, dto.MetricCode.Trim(), ct);
- if (dto.ClosePrevious)
- await ClosePreviousOpenAsync(existing, from, ct);
- var refreshed = await _store.ListActiveScopeAsync(tenantId, dto.FactoryId, dto.MetricCode.Trim(), ct);
- foreach (var row in refreshed.Where(x => x.Status == 1))
- {
- if (KpiTargetRules.Overlaps(row.EffectiveFrom, row.EffectiveTo, from, dto.EffectiveTo))
- throw new KpiTargetConflictException(
- $"与已有目标重叠:{row.EffectiveFrom:yyyy-MM-dd}~{row.EffectiveTo?.ToString("yyyy-MM-dd") ?? "开放"}");
- }
- var now = DateTime.Now;
- var created = await _store.InsertAsync(new AdoSmartOpsKpiTargetConfig
- {
- TenantId = tenantId,
- FactoryId = dto.FactoryId,
- MetricCode = dto.MetricCode.Trim(),
- TargetValue = dto.TargetValue,
- EffectiveFrom = from,
- EffectiveTo = dto.EffectiveTo?.Date,
- SourceType = sourceType,
- SourceBatchId = batchId,
- Status = 1,
- Remark = dto.Remark,
- CreatedBy = userId,
- UpdatedBy = userId,
- CreateTime = now,
- UpdateTime = now
- }, ct);
- return KpiTargetRules.ToDto(created);
- }
- public Task<KpiTargetDto> UpdateAsync(long id, KpiTargetUpdateDto dto, CancellationToken ct = default) =>
- UpdateCoreAsync(TenantId, id, dto, ct);
- public async Task<KpiTargetDto> UpdateCoreAsync(long tenantId, long id, KpiTargetUpdateDto dto, CancellationToken ct = default)
- {
- var row = await _store.GetByIdAsync(tenantId, id, ct)
- ?? throw new KpiTargetNotFoundException("目标配置不存在");
- var today = DateTime.Today;
- var alreadyEffective = row.Status == 1 && row.EffectiveFrom.Date <= today;
- if (dto.TargetValue.HasValue)
- {
- if (alreadyEffective && dto.TargetValue.Value != row.TargetValue)
- throw new KpiTargetConflictException("已生效目标不可原地改值,请新增未来版本");
- KpiTargetRules.EnsureTargetPositive(dto.TargetValue.Value);
- row.TargetValue = dto.TargetValue.Value;
- }
- if (dto.EffectiveTo.HasValue)
- {
- KpiTargetRules.EnsureRange(row.EffectiveFrom, dto.EffectiveTo);
- var others = (await _store.ListActiveScopeAsync(tenantId, row.FactoryId, row.MetricCode, ct))
- .Where(x => x.Id != row.Id && x.Status == 1);
- foreach (var other in others)
- {
- if (KpiTargetRules.Overlaps(row.EffectiveFrom, dto.EffectiveTo, other.EffectiveFrom, other.EffectiveTo))
- throw new KpiTargetConflictException("调整失效日后与其它目标重叠");
- }
- row.EffectiveTo = dto.EffectiveTo.Value.Date;
- }
- if (dto.Remark != null)
- row.Remark = dto.Remark;
- row.UpdatedBy = _userManager?.UserId;
- row.UpdateTime = DateTime.Now;
- await _store.UpdateAsync(row, ct);
- return KpiTargetRules.ToDto(row);
- }
- public async Task DisableAsync(long id, CancellationToken ct = default)
- {
- var tenantId = TenantId;
- var row = await _store.GetByIdAsync(tenantId, id, ct)
- ?? throw new KpiTargetNotFoundException("目标配置不存在");
- if (await _store.IsReferencedByDailyAsync(tenantId, id, ct))
- {
- row.Status = 0;
- row.EffectiveTo ??= DateTime.Today;
- row.UpdatedBy = _userManager.UserId;
- row.UpdateTime = DateTime.Now;
- await _store.UpdateAsync(row, ct);
- return;
- }
- row.Status = 0;
- row.UpdatedBy = _userManager.UserId;
- row.UpdateTime = DateTime.Now;
- await _store.UpdateAsync(row, ct);
- }
- public async Task<List<KpiTargetLegacyStatDto>> LegacyStatsAsync(CancellationToken ct = default)
- {
- _ = TenantId;
- _ = ct;
- return new List<KpiTargetLegacyStatDto>
- {
- new() { TargetSource = KpiTargetSnapshotSource.FormalFactory },
- new() { TargetSource = KpiTargetSnapshotSource.FormalTenantDefault },
- new() { TargetSource = KpiTargetSnapshotSource.LegacyDailyCurrent },
- new() { TargetSource = KpiTargetSnapshotSource.LegacyDailyPrior },
- new() { TargetSource = KpiTargetSnapshotSource.LegacyCode },
- new() { TargetSource = KpiTargetSnapshotSource.Missing }
- };
- }
- private async Task ClosePreviousOpenAsync(List<AdoSmartOpsKpiTargetConfig> existing, DateTime newFrom, CancellationToken ct)
- {
- var open = existing.Where(x => x.Status == 1 && x.EffectiveTo == null).OrderByDescending(x => x.EffectiveFrom).ToList();
- if (open.Count == 0)
- return;
- if (open.Count > 1)
- throw new KpiTargetDuplicateScopeException("同一工厂存在多条开放区间,拒绝静默关闭");
- var prev = open[0];
- if (prev.EffectiveFrom.Date >= newFrom)
- throw new KpiTargetConflictException("不允许自动截断未来目标");
- var closeTo = newFrom.AddDays(-1);
- if (closeTo < prev.EffectiveFrom.Date)
- throw new KpiTargetConflictException("关闭上一目标后区间无效");
- prev.EffectiveTo = closeTo;
- prev.UpdateTime = DateTime.Now;
- await _store.UpdateAsync(prev, ct);
- }
- private async Task<AdoSmartOpsKpiMaster> EnsureKpiAsync(long tenantId, string metricCode, CancellationToken ct)
- {
- var kpi = await _store.FindKpiAsync(tenantId, metricCode.Trim(), ct);
- if (kpi == null)
- throw new KpiTargetNotFoundException("指标不存在或不属于当前租户");
- return kpi;
- }
- private static void EnsureMetricCode(string? metricCode)
- {
- if (string.IsNullOrWhiteSpace(metricCode))
- throw new KpiTargetValidationException("指标编码必填");
- }
- private async Task<string?> ConfigSourceAsync(long tenantId, long configId, CancellationToken ct)
- {
- var cfg = await _store.GetByIdAsync(tenantId, configId, ct);
- return cfg?.SourceType;
- }
- public static string SerializePayload(IEnumerable<KpiTargetImportRow> rows) =>
- JsonSerializer.Serialize(rows.ToList());
- public static List<KpiTargetImportRow> DeserializePayload(string? json) =>
- string.IsNullOrWhiteSpace(json)
- ? new List<KpiTargetImportRow>()
- : JsonSerializer.Deserialize<List<KpiTargetImportRow>>(json) ?? new List<KpiTargetImportRow>();
- }
|