using Admin.NET.Core;
using Admin.NET.Plugin.AiDOP.Dto.SmartOps;
using Admin.NET.Plugin.AiDOP.Entity;
using SqlSugar;
namespace Admin.NET.Plugin.AiDOP.SmartOps;
///
/// KPI 维度计算配置服务:CRUD + 校验 + 试算 + 发布/停用/激活(发布状态机,单生效版本事务保证)。
/// 与当前生效汇总配置版本绑定;租户显式控制(ClearFilter),落在运行时 KPI 数据租户。
/// SqlScript 属高危配置,接口须登录鉴权(Controller 层)。绝不改动 SUMMARY_SQL。
///
public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
{
private const string StatusDraft = "DRAFT";
private const string StatusPublished = "PUBLISHED";
private const string StatusRetired = "RETIRED";
private static readonly string[] AggregationTypes =
{ "DIRECT_VALUE", "RATIO_OF_SUMS", "AVERAGE_OF_SUMS", "SUMMARY_ONLY" };
private readonly ISqlSugarClient _db;
private readonly KpiDimensionSqlExecutor _executor;
private readonly AdoSmartOpsKpiCalcConfigService _summaryConfig;
public AdoSmartOpsKpiDimensionConfigService(
ISqlSugarClient db, KpiDimensionSqlExecutor executor, AdoSmartOpsKpiCalcConfigService summaryConfig)
{
_db = db;
_executor = executor;
_summaryConfig = summaryConfig;
}
private ISugarQueryable Query() =>
_db.Queryable().ClearFilter();
/// 某 KPI 的全部维度配置版本(按版本号倒序)。
public async Task> GetByMetricAsync(string metricCode, string moduleCode)
{
var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
var list = await Query()
.Where(x => x.MetricCode == metricCode && x.TenantId == tenantId)
.OrderBy(x => x.DimensionConfigVersion, OrderByType.Desc)
.ToListAsync();
return list.Select(ToDto).ToList();
}
/// 当前生效维度配置(PUBLISHED + IsCurrent),无则 null。
public async Task GetActiveAsync(long tenantId, string metricCode)
{
return await Query()
.Where(x => x.MetricCode == metricCode && x.TenantId == tenantId
&& x.IsCurrent && x.PublishStatus == StatusPublished)
.FirstAsync();
}
/// 新增草稿:绑定当前生效汇总配置版本;DimensionConfigVersion = 该汇总版本下现有最大 + 1。
public async Task CreateDraftAsync(KpiDimensionConfigUpsertDto dto, string operatorName)
{
ValidateAggregation(dto.AggregationType);
var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(dto.ModuleCode);
var summary = await _summaryConfig.GetActiveAsync(tenantId, dto.MetricCode)
?? throw Oops.Bah("该 KPI 当前无生效的汇总配置(CONFIG_SQL),请先发布并激活汇总配置后再配置维度");
var maxVer = await Query()
.Where(x => x.MetricCode == dto.MetricCode && x.TenantId == tenantId && x.SummaryConfigId == summary.Id)
.MaxAsync(x => (int?)x.DimensionConfigVersion) ?? 0;
var entity = new AdoSmartOpsKpiDimensionConfig
{
TenantId = tenantId,
MetricCode = dto.MetricCode,
ModuleCode = dto.ModuleCode,
SummaryConfigId = summary.Id,
SummaryConfigVersion = summary.VersionNo,
DimensionConfigVersion = maxVer + 1,
DataSourceCode = dto.DataSourceCode,
SqlScript = dto.SqlScript,
AggregationType = dto.AggregationType,
SupportedDimensionsJson = dto.SupportedDimensionsJson,
OutputContractJson = dto.OutputContractJson,
ParameterContractJson = dto.ParameterContractJson,
TimeoutSeconds = NormalizeTimeout(dto.TimeoutSeconds),
PublishStatus = StatusDraft,
IsCurrent = false,
BusinessSqlSource = dto.BusinessSqlSource,
ChangeRemark = dto.ChangeRemark,
Remark = dto.Remark,
CreatedBy = operatorName,
CreatedAt = DateTime.Now,
};
entity.Id = await _db.Insertable(entity).ExecuteReturnBigIdentityAsync();
return ToDto(entity);
}
/// 编辑草稿(仅 DRAFT)。
public async Task UpdateDraftAsync(long id, KpiDimensionConfigUpsertDto dto, string operatorName)
{
var entity = await Query().Where(x => x.Id == id).FirstAsync()
?? throw Oops.Bah("维度配置版本不存在");
if (entity.PublishStatus != StatusDraft)
throw Oops.Bah("仅草稿(DRAFT)可编辑;已发布/停用版本请新建版本");
ValidateAggregation(dto.AggregationType);
entity.DataSourceCode = dto.DataSourceCode;
entity.SqlScript = dto.SqlScript;
entity.AggregationType = dto.AggregationType;
entity.SupportedDimensionsJson = dto.SupportedDimensionsJson;
entity.OutputContractJson = dto.OutputContractJson;
entity.ParameterContractJson = dto.ParameterContractJson;
entity.TimeoutSeconds = NormalizeTimeout(dto.TimeoutSeconds);
entity.BusinessSqlSource = dto.BusinessSqlSource;
entity.ChangeRemark = dto.ChangeRemark;
entity.Remark = dto.Remark;
entity.UpdatedBy = operatorName;
entity.UpdatedAt = DateTime.Now;
await _db.Updateable(entity).ExecuteCommandAsync();
}
/// 删除草稿(仅 DRAFT)。
public async Task DeleteAsync(long id)
{
var entity = await Query().Where(x => x.Id == id).FirstAsync()
?? throw Oops.Bah("维度配置版本不存在");
if (entity.PublishStatus != StatusDraft)
throw Oops.Bah("仅草稿(DRAFT)可删除;已发布版本请用停用");
await _db.Deleteable().Where(x => x.Id == id).ExecuteCommandAsync();
}
/// SQL 安全校验(复用汇总配置的多层非正则校验器)。
public KpiSqlValidateResultDto Validate(string? sql)
{
var r = KpiSqlSecurityValidator.Validate(sql);
return new KpiSqlValidateResultDto
{
Ok = r.Ok,
ValidationStatus = r.Ok ? "VALID" : "INVALID",
ErrorCode = r.ErrorCode,
ErrorMessage = r.ErrorMessage,
ReferencedTables = r.ReferencedTables,
};
}
/// 试算(服务端重校验 + 只读多行执行;不写维度结果)。返回前 20 行样本 + 输出列 + 可识别维度。
public async Task PreviewAsync(KpiDimensionPreviewDto dto, CancellationToken ct)
{
var bizDate = (dto.BizDate ?? DateTime.Today.AddDays(-1)).Date;
var res = new KpiDimensionPreviewResultDto
{
MetricCode = dto.MetricCode,
DataSourceCode = dto.DataSourceCode,
BizDate = bizDate.ToString("yyyy-MM-dd"),
};
var val = KpiSqlSecurityValidator.Validate(dto.SqlScript);
res.ValidationStatus = val.Ok ? "VALID" : "INVALID";
if (!val.Ok)
{
res.Ok = false;
res.ResultStatus = "FAILED";
res.ErrorCode = val.ErrorCode;
res.ErrorMessage = val.ErrorMessage;
return res;
}
var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(dto.ModuleCode);
var pars = BuildRunParams(tenantId, dto.ModuleCode, dto.MetricCode, bizDate);
var exec = await _executor.ExecuteAsync(dto.DataSourceCode, dto.SqlScript ?? "", dto.TimeoutSeconds, true, pars, ct);
res.Ok = exec.Status != "FAILED";
res.ResultStatus = exec.Status;
res.DurationMs = exec.DurationMs;
res.RowCount = exec.RowCount;
res.OutputColumns = exec.OutputColumns;
res.SupportedDimensions = DetectSupportedDimensions(exec.OutputColumns);
res.ErrorCode = exec.ErrorCode;
res.ErrorMessage = exec.ErrorMessage;
res.SampleRows = exec.Rows.Take(20).Select(ToSampleRow).ToList();
res.ExecutedParameters =
$"tenant_id={pars.TenantId}, factory_id={pars.FactoryId}, module_code={pars.ModuleCode}, " +
$"metric_code={pars.MetricCode}, biz_date={pars.BizDate:yyyy-MM-dd}, ztid={pars.SourceZtid}";
return res;
}
/// 发布:绑定汇总版本一致性校验 + 安全校验 + 事务单生效版本。
public async Task PublishAsync(long id, string operatorName)
{
var entity = await Query().Where(x => x.Id == id).FirstAsync()
?? throw Oops.Bah("维度配置版本不存在");
if (entity.PublishStatus == StatusRetired)
throw Oops.Bah("已停用版本不可发布,请新建版本");
ValidateAggregation(entity.AggregationType);
if (entity.AggregationType != "SUMMARY_ONLY")
{
var val = KpiSqlSecurityValidator.Validate(entity.SqlScript);
if (!val.Ok) throw Oops.Bah($"SQL 安全校验未通过:{val.ErrorCode} {val.ErrorMessage}");
if (string.IsNullOrWhiteSpace(entity.DataSourceCode))
throw Oops.Bah("非 SUMMARY_ONLY 的维度配置必须指定数据源");
if (string.IsNullOrWhiteSpace(entity.SupportedDimensionsJson))
throw Oops.Bah("必须声明支持维度(SupportedDimensions)");
}
// 绑定的汇总配置必须仍存在且为当前生效版本
var summary = await _summaryConfig.GetActiveAsync(entity.TenantId ?? 0, entity.MetricCode)
?? throw Oops.Bah("绑定的汇总配置已不存在或未生效,请基于当前汇总版本新建维度配置");
if (summary.Id != entity.SummaryConfigId || summary.VersionNo != entity.SummaryConfigVersion)
throw Oops.Bah($"汇总配置已切换到新版本(v{summary.VersionNo}),本维度配置绑定的是 v{entity.SummaryConfigVersion},请基于当前汇总版本新建维度配置");
var tenantId = entity.TenantId;
var tran = await _db.AsTenant().UseTranAsync(async () =>
{
await _db.Updateable()
.SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = false })
.Where(x => x.MetricCode == entity.MetricCode && x.TenantId == tenantId && x.Id != id && x.IsCurrent)
.ExecuteCommandAsync();
await _db.Updateable()
.SetColumns(x => new AdoSmartOpsKpiDimensionConfig
{
PublishStatus = StatusPublished, IsCurrent = true,
PublishedBy = operatorName, PublishedAt = DateTime.Now,
})
.Where(x => x.Id == id)
.ExecuteCommandAsync();
});
if (!tran.IsSuccess) throw tran.ErrorException;
}
/// 激活/回滚:把某已发布维度版本设为当前生效。
public async Task ActivateAsync(long id, string operatorName)
{
var entity = await Query().Where(x => x.Id == id).FirstAsync()
?? throw Oops.Bah("维度配置版本不存在");
if (entity.PublishStatus != StatusPublished)
throw Oops.Bah("只能激活已发布(PUBLISHED)版本");
var tenantId = entity.TenantId;
var tran = await _db.AsTenant().UseTranAsync(async () =>
{
await _db.Updateable()
.SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = false })
.Where(x => x.MetricCode == entity.MetricCode && x.TenantId == tenantId && x.Id != id && x.IsCurrent)
.ExecuteCommandAsync();
await _db.Updateable()
.SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = true, UpdatedBy = operatorName, UpdatedAt = DateTime.Now })
.Where(x => x.Id == id)
.ExecuteCommandAsync();
});
if (!tran.IsSuccess) throw tran.ErrorException;
}
/// 停用(RETIRED,IsCurrent=0)。停用当前生效版本后无 current → 筛选/下钻显示未配置。
public async Task RetireAsync(long id, string operatorName)
{
var entity = await Query().Where(x => x.Id == id).FirstAsync()
?? throw Oops.Bah("维度配置版本不存在");
await _db.Updateable()
.SetColumns(x => new AdoSmartOpsKpiDimensionConfig
{
PublishStatus = StatusRetired, IsCurrent = false,
RetiredBy = operatorName, RetiredAt = DateTime.Now,
})
.Where(x => x.Id == id)
.ExecuteCommandAsync();
}
/// KPI 维度能力:无生效维度配置 → PENDING_DIMENSION_SQL;SUMMARY_ONLY → SUMMARY_ONLY;否则 CONFIGURED。
public async Task GetCapabilityAsync(string metricCode, string moduleCode)
{
var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
var active = await GetActiveAsync(tenantId, metricCode);
var cap = new KpiDimensionCapabilityDto { MetricCode = metricCode, ModuleCode = moduleCode };
if (active == null)
{
cap.Supported = false;
cap.CapabilityStatus = "PENDING_DIMENSION_SQL";
return cap;
}
cap.SummaryConfigVersion = active.SummaryConfigVersion;
cap.DimensionConfigVersion = active.DimensionConfigVersion;
cap.AggregationType = active.AggregationType;
cap.SupportedDimensions = ParseDimensions(active.SupportedDimensionsJson);
cap.Supported = active.AggregationType != "SUMMARY_ONLY";
cap.CapabilityStatus = active.AggregationType == "SUMMARY_ONLY" ? "SUMMARY_ONLY" : "CONFIGURED";
var lastRun = await _db.Queryable().ClearFilter()
.Where(x => x.MetricCode == metricCode && x.TenantId == tenantId)
.OrderBy(x => x.StartedAt, OrderByType.Desc)
.FirstAsync();
if (lastRun != null)
{
cap.LastRunStatus = lastRun.Status;
cap.LastRunAt = lastRun.StartedAt;
}
return cap;
}
///
/// 维度明细只读查询(当前生效维度版本):按 AggregationType 复算聚合值 + 分页明细。
/// 只读当前激活配置产生的维度结果,绝不读旧 Atomic。无激活配置 → NOT_CONFIGURED。
///
public async Task QueryDetailAsync(
string metricCode, string moduleCode, DateTime? startDate, DateTime? endDate,
string? dimensionType, string? dimensionCode, int page, int pageSize)
{
var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
var active = await GetActiveAsync(tenantId, metricCode);
var result = new KpiDimensionDetailResultDto { MetricCode = metricCode };
if (active == null)
{
result.Status = "NOT_CONFIGURED";
return result;
}
result.SummaryConfigVersion = active.SummaryConfigVersion;
result.DimensionConfigVersion = active.DimensionConfigVersion;
result.AggregationType = active.AggregationType;
var sd = startDate?.Date;
var ed = endDate?.Date;
ISugarQueryable Build() =>
_db.Queryable().ClearFilter()
.Where(x => x.TenantId == tenantId && x.MetricCode == metricCode
&& x.DimensionConfigVersion == active.DimensionConfigVersion)
.WhereIF(sd.HasValue, x => x.ValueDate >= sd!.Value)
.WhereIF(ed.HasValue, x => x.ValueDate <= ed!.Value)
.WhereIF(!string.IsNullOrWhiteSpace(dimensionType), x => x.DimensionType == dimensionType)
.WhereIF(!string.IsNullOrWhiteSpace(dimensionCode), x => x.DimensionCode == dimensionCode);
result.AggregateValue = active.AggregationType switch
{
"AVERAGE_OF_SUMS" => Div(await Build().SumAsync(x => x.SumValue), (decimal?)await Build().SumAsync(x => x.SampleCount)),
"RATIO_OF_SUMS" => Div(await Build().SumAsync(x => x.Numerator), await Build().SumAsync(x => x.Denominator)),
"DIRECT_VALUE" => await Build().AvgAsync(x => x.MetricValue),
_ => null,
};
RefAsync total = 0;
var list = await Build().OrderBy(x => x.ValueDate, OrderByType.Desc).OrderBy(x => x.DimensionCode)
.ToPageListAsync(page <= 0 ? 1 : page, pageSize <= 0 ? 20 : pageSize, total);
result.Total = total.Value;
result.List = list.Select(x => new KpiDimensionDetailRowDto
{
ValueDate = x.ValueDate.ToString("yyyy-MM-dd"),
DimensionType = x.DimensionType,
DimensionCode = x.DimensionCode,
DimensionName = x.DimensionName,
MetricValue = x.MetricValue,
Numerator = x.Numerator,
Denominator = x.Denominator,
SumValue = x.SumValue,
SampleCount = x.SampleCount,
SourceKey = x.SourceKey,
BatchId = x.BatchId,
}).ToList();
return result;
}
private static decimal? Div(decimal? a, decimal? b) =>
b.HasValue && b.Value != 0 ? (a ?? 0m) / b.Value : null;
/// 已登记数据源列表(第一版仅本地中台库)。
public List