| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- using System.Security.Cryptography;
- using System.Text;
- using Admin.NET.Core;
- using Admin.NET.Plugin.AiDOP.Entity;
- using SqlSugar;
- namespace Admin.NET.Plugin.AiDOP.SmartOps;
- /// <summary>维度执行结果概要(供调度/接口回执)。</summary>
- public sealed class KpiDimensionRunResult
- {
- public string Status { get; set; } = string.Empty;
- public int RowCount { get; set; }
- public string? ErrorCode { get; set; }
- public string? ErrorMessage { get; set; }
- }
- /// <summary>
- /// KPI 维度执行通用入口:读当前生效维度配置 → 校验绑定汇总版本一致 → 只读执行 DIMENSION_SQL
- /// → 事务 FULL REPLACE 写维度结果表 → 写运行日志。禁止 per-KPI 分支。
- /// </summary>
- public sealed class KpiDimensionRunService : ITransient
- {
- private readonly ISqlSugarClient _db;
- private readonly KpiDimensionSqlExecutor _executor;
- private readonly AdoSmartOpsKpiDimensionConfigService _dimensionConfig;
- private readonly AdoSmartOpsKpiCalcConfigService _summaryConfig;
- public KpiDimensionRunService(
- ISqlSugarClient db, KpiDimensionSqlExecutor executor,
- AdoSmartOpsKpiDimensionConfigService dimensionConfig, AdoSmartOpsKpiCalcConfigService summaryConfig)
- {
- _db = db;
- _executor = executor;
- _dimensionConfig = dimensionConfig;
- _summaryConfig = summaryConfig;
- }
- /// <summary>
- /// 执行某 KPI 当前生效维度配置。SUMMARY_ONLY / 无配置 / 版本不匹配均只记日志、不写结果。
- /// </summary>
- public async Task<KpiDimensionRunResult> RunDimensionAsync(
- string metricCode, string moduleCode, long tenantId, DateTime valueDate,
- string batchId, string triggerType, CancellationToken ct)
- {
- var startedAt = DateTime.Now;
- var bizDate = valueDate.Date;
- var active = await _dimensionConfig.GetActiveAsync(tenantId, metricCode);
- if (active == null)
- {
- await WriteRunLogAsync(null, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt,
- "NOT_CONFIGURED", 0, "NOT_CONFIGURED", "无生效维度配置", null);
- return new KpiDimensionRunResult { Status = "NOT_CONFIGURED", ErrorCode = "NOT_CONFIGURED" };
- }
- if (active.AggregationType == "SUMMARY_ONLY")
- {
- await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt,
- "NOT_CONFIGURED", 0, "SUMMARY_ONLY", "维度配置为 SUMMARY_ONLY,不产生维度明细", null);
- return new KpiDimensionRunResult { Status = "NOT_CONFIGURED", ErrorCode = "SUMMARY_ONLY" };
- }
- // 绑定汇总版本一致性校验
- var summary = await _summaryConfig.GetActiveAsync(tenantId, metricCode);
- if (summary == null || summary.Id != active.SummaryConfigId || summary.VersionNo != active.SummaryConfigVersion)
- {
- await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt,
- "VERSION_MISMATCH", 0, "VERSION_MISMATCH", "维度配置绑定的汇总版本与当前生效汇总版本不一致", null);
- return new KpiDimensionRunResult { Status = "VERSION_MISMATCH", ErrorCode = "VERSION_MISMATCH" };
- }
- var pars = AdoSmartOpsKpiDimensionConfigService.BuildRunParams(tenantId, moduleCode, metricCode, bizDate);
- var exec = await _executor.ExecuteAsync(active.DataSourceCode, active.SqlScript ?? "", active.TimeoutSeconds, false, pars, ct);
- if (exec.Status == "FAILED")
- {
- await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt,
- "FAILED", exec.RowCount, exec.ErrorCode, exec.ErrorMessage, exec.SqlHash);
- return new KpiDimensionRunResult { Status = "FAILED", RowCount = exec.RowCount, ErrorCode = exec.ErrorCode, ErrorMessage = exec.ErrorMessage };
- }
- // FULL REPLACE:同 (tenant, metric, dimVersion) + 本次返回的各 value_date 范围内先删后插,事务提交。
- var now = DateTime.Now;
- var rows = exec.Rows.Select(r => ToEntity(r, active, tenantId, metricCode, moduleCode, batchId, now)).ToList();
- var dates = rows.Select(x => x.ValueDate.Date).Distinct().ToList();
- if (dates.Count == 0) dates.Add(bizDate); // NO_DATA 也清空当日该版本旧数据
- var tran = await _db.AsTenant().UseTranAsync(async () =>
- {
- await _db.Deleteable<AdoSmartOpsKpiDimensionValueDay>()
- .Where(x => x.TenantId == tenantId && x.MetricCode == metricCode
- && x.DimensionConfigVersion == active.DimensionConfigVersion
- && dates.Contains(x.ValueDate.Date))
- .ExecuteCommandAsync();
- if (rows.Count > 0)
- await _db.Insertable(rows).ExecuteCommandAsync();
- });
- if (!tran.IsSuccess)
- {
- await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt,
- "FAILED", rows.Count, "WRITE_FAILED", tran.ErrorException?.Message, exec.SqlHash);
- return new KpiDimensionRunResult { Status = "FAILED", ErrorCode = "WRITE_FAILED", ErrorMessage = tran.ErrorException?.Message };
- }
- var status = rows.Count == 0 ? "NO_DATA" : "SUCCESS";
- await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt,
- status, rows.Count, null, null, exec.SqlHash);
- return new KpiDimensionRunResult { Status = status, RowCount = rows.Count };
- }
- private AdoSmartOpsKpiDimensionValueDay ToEntity(
- KpiDimensionRow r, AdoSmartOpsKpiDimensionConfig cfg, long tenantId, string metricCode, string moduleCode,
- string batchId, DateTime now)
- {
- var valueDate = r.ValueDate == DateTime.MinValue ? DateTime.Today : r.ValueDate.Date;
- var e = new AdoSmartOpsKpiDimensionValueDay
- {
- TenantId = tenantId,
- ModuleCode = moduleCode,
- MetricCode = metricCode,
- SummaryConfigId = cfg.SummaryConfigId,
- SummaryConfigVersion = cfg.SummaryConfigVersion,
- DimensionConfigId = cfg.Id,
- DimensionConfigVersion = cfg.DimensionConfigVersion,
- DataSourceCode = cfg.DataSourceCode,
- ValueDate = valueDate,
- DimensionType = r.DimensionType,
- DimensionCode = r.DimensionCode,
- DimensionName = r.DimensionName,
- OrgId = r.OrgId,
- FactoryId = r.FactoryId ?? 1,
- MaterialCode = r.MaterialCode,
- WorkOrderNo = r.WorkOrderNo,
- CategoryCode = r.CategoryCode,
- WarehouseCode = r.WarehouseCode,
- OrderNo = r.OrderNo,
- CustomerCode = r.CustomerCode,
- ProductCode = r.ProductCode,
- EquipmentCode = r.EquipmentCode,
- MetricValue = r.MetricValue,
- Numerator = r.Numerator,
- Denominator = r.Denominator,
- SumValue = r.SumValue,
- SampleCount = r.SampleCount,
- SourceKey = r.SourceKey,
- BatchId = batchId,
- CalcTime = now,
- CreatedTime = now,
- };
- e.RowHash = ComputeRowHash(tenantId, metricCode, cfg.DimensionConfigVersion, valueDate, r.DimensionType, r.DimensionCode, r.SourceKey);
- return e;
- }
- private static string ComputeRowHash(long tenantId, string metricCode, int dimVersion, DateTime valueDate,
- string dimType, string dimCode, string? sourceKey)
- {
- var raw = $"{tenantId}|{metricCode}|{dimVersion}|{valueDate:yyyyMMdd}|{dimType}|{dimCode}|{sourceKey}";
- return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw)));
- }
- private async Task WriteRunLogAsync(
- AdoSmartOpsKpiDimensionConfig? cfg, string metricCode, string moduleCode, long tenantId,
- string batchId, string triggerType, DateTime bizDate, DateTime startedAt,
- string status, int rowCount, string? errorCode, string? errorMessage, string? sqlHash)
- {
- try
- {
- var finishedAt = DateTime.Now;
- var log = new AdoSmartOpsKpiDimensionRunLog
- {
- TenantId = tenantId,
- BatchId = batchId,
- ModuleCode = moduleCode,
- MetricCode = metricCode,
- DimensionConfigId = cfg?.Id,
- DimensionConfigVersion = cfg?.DimensionConfigVersion,
- SummaryConfigVersion = cfg?.SummaryConfigVersion,
- DataSourceCode = cfg?.DataSourceCode,
- BizDate = bizDate.Date,
- StartedAt = startedAt,
- FinishedAt = finishedAt,
- DurationMs = (long)(finishedAt - startedAt).TotalMilliseconds,
- Status = status,
- RowCount = rowCount,
- ErrorCode = errorCode,
- ErrorMessage = errorMessage == null ? null : (errorMessage.Length > 480 ? errorMessage.Substring(0, 480) : errorMessage),
- SqlHash = sqlHash,
- TriggerType = triggerType,
- CreateTime = finishedAt,
- };
- var existing = await _db.Queryable<AdoSmartOpsKpiDimensionRunLog>().ClearFilter<ITenantIdFilter>()
- .Where(x => x.MetricCode == metricCode && x.BatchId == batchId).FirstAsync();
- if (existing != null)
- {
- log.Id = existing.Id;
- await _db.Updateable(log).ExecuteCommandAsync();
- }
- else
- {
- await _db.Insertable(log).ExecuteCommandAsync();
- }
- }
- catch
- {
- // 运行日志失败不影响主执行链路
- }
- }
- }
|