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;
/// 维度执行结果概要(供调度/接口回执)。
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; }
}
///
/// KPI 维度执行通用入口:读当前生效维度配置 → 校验绑定汇总版本一致 → 只读执行 DIMENSION_SQL
/// → 事务 FULL REPLACE 写维度结果表 → 写运行日志。禁止 per-KPI 分支。
///
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;
}
///
/// 执行某 KPI 当前生效维度配置。SUMMARY_ONLY / 无配置 / 版本不匹配均只记日志、不写结果。
///
public async Task 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()
.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().ClearFilter()
.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
{
// 运行日志失败不影响主执行链路
}
}
}