AdoSmartOpsKpiDimensionConfigService.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Dto.SmartOps;
  3. using Admin.NET.Plugin.AiDOP.Entity;
  4. using SqlSugar;
  5. namespace Admin.NET.Plugin.AiDOP.SmartOps;
  6. /// <summary>
  7. /// KPI 维度计算配置服务:CRUD + 校验 + 试算 + 发布/停用/激活(发布状态机,单生效版本事务保证)。
  8. /// 与当前生效汇总配置版本绑定;租户显式控制(ClearFilter),落在运行时 KPI 数据租户。
  9. /// SqlScript 属高危配置,接口须登录鉴权(Controller 层)。绝不改动 SUMMARY_SQL。
  10. /// </summary>
  11. public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
  12. {
  13. private const string StatusDraft = "DRAFT";
  14. private const string StatusPublished = "PUBLISHED";
  15. private const string StatusRetired = "RETIRED";
  16. private static readonly string[] AggregationTypes =
  17. { "DIRECT_VALUE", "RATIO_OF_SUMS", "AVERAGE_OF_SUMS", "SUMMARY_ONLY" };
  18. private readonly ISqlSugarClient _db;
  19. private readonly KpiDimensionSqlExecutor _executor;
  20. private readonly AdoSmartOpsKpiCalcConfigService _summaryConfig;
  21. public AdoSmartOpsKpiDimensionConfigService(
  22. ISqlSugarClient db, KpiDimensionSqlExecutor executor, AdoSmartOpsKpiCalcConfigService summaryConfig)
  23. {
  24. _db = db;
  25. _executor = executor;
  26. _summaryConfig = summaryConfig;
  27. }
  28. private ISugarQueryable<AdoSmartOpsKpiDimensionConfig> Query() =>
  29. _db.Queryable<AdoSmartOpsKpiDimensionConfig>().ClearFilter<ITenantIdFilter>();
  30. /// <summary>某 KPI 的全部维度配置版本(按版本号倒序)。</summary>
  31. public async Task<List<KpiDimensionConfigDto>> GetByMetricAsync(string metricCode, string moduleCode)
  32. {
  33. var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
  34. var list = await Query()
  35. .Where(x => x.MetricCode == metricCode && x.TenantId == tenantId)
  36. .OrderBy(x => x.DimensionConfigVersion, OrderByType.Desc)
  37. .ToListAsync();
  38. return list.Select(ToDto).ToList();
  39. }
  40. /// <summary>当前生效维度配置(PUBLISHED + IsCurrent),无则 null。</summary>
  41. public async Task<AdoSmartOpsKpiDimensionConfig?> GetActiveAsync(long tenantId, string metricCode)
  42. {
  43. return await Query()
  44. .Where(x => x.MetricCode == metricCode && x.TenantId == tenantId
  45. && x.IsCurrent && x.PublishStatus == StatusPublished)
  46. .FirstAsync();
  47. }
  48. /// <summary>新增草稿:绑定当前生效汇总配置版本;DimensionConfigVersion = 该汇总版本下现有最大 + 1。</summary>
  49. public async Task<KpiDimensionConfigDto> CreateDraftAsync(KpiDimensionConfigUpsertDto dto, string operatorName)
  50. {
  51. ValidateAggregation(dto.AggregationType);
  52. var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(dto.ModuleCode);
  53. var summary = await _summaryConfig.GetActiveAsync(tenantId, dto.MetricCode)
  54. ?? throw Oops.Bah("该 KPI 当前无生效的汇总配置(CONFIG_SQL),请先发布并激活汇总配置后再配置维度");
  55. var maxVer = await Query()
  56. .Where(x => x.MetricCode == dto.MetricCode && x.TenantId == tenantId && x.SummaryConfigId == summary.Id)
  57. .MaxAsync(x => (int?)x.DimensionConfigVersion) ?? 0;
  58. var entity = new AdoSmartOpsKpiDimensionConfig
  59. {
  60. TenantId = tenantId,
  61. MetricCode = dto.MetricCode,
  62. ModuleCode = dto.ModuleCode,
  63. SummaryConfigId = summary.Id,
  64. SummaryConfigVersion = summary.VersionNo,
  65. DimensionConfigVersion = maxVer + 1,
  66. DataSourceCode = dto.DataSourceCode,
  67. SqlScript = dto.SqlScript,
  68. AggregationType = dto.AggregationType,
  69. SupportedDimensionsJson = dto.SupportedDimensionsJson,
  70. OutputContractJson = dto.OutputContractJson,
  71. ParameterContractJson = dto.ParameterContractJson,
  72. TimeoutSeconds = NormalizeTimeout(dto.TimeoutSeconds),
  73. PublishStatus = StatusDraft,
  74. IsCurrent = false,
  75. BusinessSqlSource = dto.BusinessSqlSource,
  76. ChangeRemark = dto.ChangeRemark,
  77. Remark = dto.Remark,
  78. CreatedBy = operatorName,
  79. CreatedAt = DateTime.Now,
  80. };
  81. entity.Id = await _db.Insertable(entity).ExecuteReturnBigIdentityAsync();
  82. return ToDto(entity);
  83. }
  84. /// <summary>编辑草稿(仅 DRAFT)。</summary>
  85. public async Task UpdateDraftAsync(long id, KpiDimensionConfigUpsertDto dto, string operatorName)
  86. {
  87. var entity = await Query().Where(x => x.Id == id).FirstAsync()
  88. ?? throw Oops.Bah("维度配置版本不存在");
  89. if (entity.PublishStatus != StatusDraft)
  90. throw Oops.Bah("仅草稿(DRAFT)可编辑;已发布/停用版本请新建版本");
  91. ValidateAggregation(dto.AggregationType);
  92. entity.DataSourceCode = dto.DataSourceCode;
  93. entity.SqlScript = dto.SqlScript;
  94. entity.AggregationType = dto.AggregationType;
  95. entity.SupportedDimensionsJson = dto.SupportedDimensionsJson;
  96. entity.OutputContractJson = dto.OutputContractJson;
  97. entity.ParameterContractJson = dto.ParameterContractJson;
  98. entity.TimeoutSeconds = NormalizeTimeout(dto.TimeoutSeconds);
  99. entity.BusinessSqlSource = dto.BusinessSqlSource;
  100. entity.ChangeRemark = dto.ChangeRemark;
  101. entity.Remark = dto.Remark;
  102. entity.UpdatedBy = operatorName;
  103. entity.UpdatedAt = DateTime.Now;
  104. await _db.Updateable(entity).ExecuteCommandAsync();
  105. }
  106. /// <summary>删除草稿(仅 DRAFT)。</summary>
  107. public async Task DeleteAsync(long id)
  108. {
  109. var entity = await Query().Where(x => x.Id == id).FirstAsync()
  110. ?? throw Oops.Bah("维度配置版本不存在");
  111. if (entity.PublishStatus != StatusDraft)
  112. throw Oops.Bah("仅草稿(DRAFT)可删除;已发布版本请用停用");
  113. await _db.Deleteable<AdoSmartOpsKpiDimensionConfig>().Where(x => x.Id == id).ExecuteCommandAsync();
  114. }
  115. /// <summary>SQL 安全校验(复用汇总配置的多层非正则校验器)。</summary>
  116. public KpiSqlValidateResultDto Validate(string? sql)
  117. {
  118. var r = KpiSqlSecurityValidator.Validate(sql);
  119. return new KpiSqlValidateResultDto
  120. {
  121. Ok = r.Ok,
  122. ValidationStatus = r.Ok ? "VALID" : "INVALID",
  123. ErrorCode = r.ErrorCode,
  124. ErrorMessage = r.ErrorMessage,
  125. ReferencedTables = r.ReferencedTables,
  126. };
  127. }
  128. /// <summary>试算(服务端重校验 + 只读多行执行;不写维度结果)。返回前 20 行样本 + 输出列 + 可识别维度。</summary>
  129. public async Task<KpiDimensionPreviewResultDto> PreviewAsync(KpiDimensionPreviewDto dto, CancellationToken ct)
  130. {
  131. var bizDate = (dto.BizDate ?? DateTime.Today.AddDays(-1)).Date;
  132. var res = new KpiDimensionPreviewResultDto
  133. {
  134. MetricCode = dto.MetricCode,
  135. DataSourceCode = dto.DataSourceCode,
  136. BizDate = bizDate.ToString("yyyy-MM-dd"),
  137. };
  138. var val = KpiSqlSecurityValidator.Validate(dto.SqlScript);
  139. res.ValidationStatus = val.Ok ? "VALID" : "INVALID";
  140. if (!val.Ok)
  141. {
  142. res.Ok = false;
  143. res.ResultStatus = "FAILED";
  144. res.ErrorCode = val.ErrorCode;
  145. res.ErrorMessage = val.ErrorMessage;
  146. return res;
  147. }
  148. var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(dto.ModuleCode);
  149. var pars = BuildRunParams(tenantId, dto.ModuleCode, dto.MetricCode, bizDate);
  150. var exec = await _executor.ExecuteAsync(dto.DataSourceCode, dto.SqlScript ?? "", dto.TimeoutSeconds, true, pars, ct);
  151. res.Ok = exec.Status != "FAILED";
  152. res.ResultStatus = exec.Status;
  153. res.DurationMs = exec.DurationMs;
  154. res.RowCount = exec.RowCount;
  155. res.OutputColumns = exec.OutputColumns;
  156. res.SupportedDimensions = DetectSupportedDimensions(exec.OutputColumns);
  157. res.ErrorCode = exec.ErrorCode;
  158. res.ErrorMessage = exec.ErrorMessage;
  159. res.SampleRows = exec.Rows.Take(20).Select(ToSampleRow).ToList();
  160. res.ExecutedParameters =
  161. $"tenant_id={pars.TenantId}, factory_id={pars.FactoryId}, module_code={pars.ModuleCode}, " +
  162. $"metric_code={pars.MetricCode}, biz_date={pars.BizDate:yyyy-MM-dd}, ztid={pars.SourceZtid}";
  163. return res;
  164. }
  165. /// <summary>发布:绑定汇总版本一致性校验 + 安全校验 + 事务单生效版本。</summary>
  166. public async Task PublishAsync(long id, string operatorName)
  167. {
  168. var entity = await Query().Where(x => x.Id == id).FirstAsync()
  169. ?? throw Oops.Bah("维度配置版本不存在");
  170. if (entity.PublishStatus == StatusRetired)
  171. throw Oops.Bah("已停用版本不可发布,请新建版本");
  172. ValidateAggregation(entity.AggregationType);
  173. if (entity.AggregationType != "SUMMARY_ONLY")
  174. {
  175. var val = KpiSqlSecurityValidator.Validate(entity.SqlScript);
  176. if (!val.Ok) throw Oops.Bah($"SQL 安全校验未通过:{val.ErrorCode} {val.ErrorMessage}");
  177. if (string.IsNullOrWhiteSpace(entity.DataSourceCode))
  178. throw Oops.Bah("非 SUMMARY_ONLY 的维度配置必须指定数据源");
  179. if (string.IsNullOrWhiteSpace(entity.SupportedDimensionsJson))
  180. throw Oops.Bah("必须声明支持维度(SupportedDimensions)");
  181. }
  182. // 绑定的汇总配置必须仍存在且为当前生效版本
  183. var summary = await _summaryConfig.GetActiveAsync(entity.TenantId ?? 0, entity.MetricCode)
  184. ?? throw Oops.Bah("绑定的汇总配置已不存在或未生效,请基于当前汇总版本新建维度配置");
  185. if (summary.Id != entity.SummaryConfigId || summary.VersionNo != entity.SummaryConfigVersion)
  186. throw Oops.Bah($"汇总配置已切换到新版本(v{summary.VersionNo}),本维度配置绑定的是 v{entity.SummaryConfigVersion},请基于当前汇总版本新建维度配置");
  187. var tenantId = entity.TenantId;
  188. var tran = await _db.AsTenant().UseTranAsync(async () =>
  189. {
  190. await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
  191. .SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = false })
  192. .Where(x => x.MetricCode == entity.MetricCode && x.TenantId == tenantId && x.Id != id && x.IsCurrent)
  193. .ExecuteCommandAsync();
  194. await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
  195. .SetColumns(x => new AdoSmartOpsKpiDimensionConfig
  196. {
  197. PublishStatus = StatusPublished, IsCurrent = true,
  198. PublishedBy = operatorName, PublishedAt = DateTime.Now,
  199. })
  200. .Where(x => x.Id == id)
  201. .ExecuteCommandAsync();
  202. });
  203. if (!tran.IsSuccess) throw tran.ErrorException;
  204. }
  205. /// <summary>激活/回滚:把某已发布维度版本设为当前生效。</summary>
  206. public async Task ActivateAsync(long id, string operatorName)
  207. {
  208. var entity = await Query().Where(x => x.Id == id).FirstAsync()
  209. ?? throw Oops.Bah("维度配置版本不存在");
  210. if (entity.PublishStatus != StatusPublished)
  211. throw Oops.Bah("只能激活已发布(PUBLISHED)版本");
  212. var tenantId = entity.TenantId;
  213. var tran = await _db.AsTenant().UseTranAsync(async () =>
  214. {
  215. await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
  216. .SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = false })
  217. .Where(x => x.MetricCode == entity.MetricCode && x.TenantId == tenantId && x.Id != id && x.IsCurrent)
  218. .ExecuteCommandAsync();
  219. await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
  220. .SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = true, UpdatedBy = operatorName, UpdatedAt = DateTime.Now })
  221. .Where(x => x.Id == id)
  222. .ExecuteCommandAsync();
  223. });
  224. if (!tran.IsSuccess) throw tran.ErrorException;
  225. }
  226. /// <summary>停用(RETIRED,IsCurrent=0)。停用当前生效版本后无 current → 筛选/下钻显示未配置。</summary>
  227. public async Task RetireAsync(long id, string operatorName)
  228. {
  229. var entity = await Query().Where(x => x.Id == id).FirstAsync()
  230. ?? throw Oops.Bah("维度配置版本不存在");
  231. await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
  232. .SetColumns(x => new AdoSmartOpsKpiDimensionConfig
  233. {
  234. PublishStatus = StatusRetired, IsCurrent = false,
  235. RetiredBy = operatorName, RetiredAt = DateTime.Now,
  236. })
  237. .Where(x => x.Id == id)
  238. .ExecuteCommandAsync();
  239. }
  240. /// <summary>KPI 维度能力:无生效维度配置 → PENDING_DIMENSION_SQL;SUMMARY_ONLY → SUMMARY_ONLY;否则 CONFIGURED。</summary>
  241. public async Task<KpiDimensionCapabilityDto> GetCapabilityAsync(string metricCode, string moduleCode)
  242. {
  243. var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
  244. var active = await GetActiveAsync(tenantId, metricCode);
  245. var cap = new KpiDimensionCapabilityDto { MetricCode = metricCode, ModuleCode = moduleCode };
  246. if (active == null)
  247. {
  248. cap.Supported = false;
  249. cap.CapabilityStatus = "PENDING_DIMENSION_SQL";
  250. return cap;
  251. }
  252. cap.SummaryConfigVersion = active.SummaryConfigVersion;
  253. cap.DimensionConfigVersion = active.DimensionConfigVersion;
  254. cap.AggregationType = active.AggregationType;
  255. cap.SupportedDimensions = ParseDimensions(active.SupportedDimensionsJson);
  256. cap.Supported = active.AggregationType != "SUMMARY_ONLY";
  257. cap.CapabilityStatus = active.AggregationType == "SUMMARY_ONLY" ? "SUMMARY_ONLY" : "CONFIGURED";
  258. var lastRun = await _db.Queryable<AdoSmartOpsKpiDimensionRunLog>().ClearFilter<ITenantIdFilter>()
  259. .Where(x => x.MetricCode == metricCode && x.TenantId == tenantId)
  260. .OrderBy(x => x.StartedAt, OrderByType.Desc)
  261. .FirstAsync();
  262. if (lastRun != null)
  263. {
  264. cap.LastRunStatus = lastRun.Status;
  265. cap.LastRunAt = lastRun.StartedAt;
  266. }
  267. return cap;
  268. }
  269. /// <summary>
  270. /// 维度明细只读查询(当前生效维度版本):按 AggregationType 复算聚合值 + 分页明细。
  271. /// 只读当前激活配置产生的维度结果,绝不读旧 Atomic。无激活配置 → NOT_CONFIGURED。
  272. /// </summary>
  273. public async Task<KpiDimensionDetailResultDto> QueryDetailAsync(
  274. string metricCode, string moduleCode, DateTime? startDate, DateTime? endDate,
  275. string? dimensionType, string? dimensionCode, int page, int pageSize,
  276. string? orderNo = null, string? sortField = null, string? sortOrder = null)
  277. {
  278. var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
  279. var active = await GetActiveAsync(tenantId, metricCode);
  280. var result = new KpiDimensionDetailResultDto { MetricCode = metricCode };
  281. if (active == null)
  282. {
  283. result.Status = "NOT_CONFIGURED";
  284. return result;
  285. }
  286. result.SummaryConfigVersion = active.SummaryConfigVersion;
  287. result.DimensionConfigVersion = active.DimensionConfigVersion;
  288. result.AggregationType = active.AggregationType;
  289. var sd = startDate?.Date;
  290. var ed = endDate?.Date;
  291. var ordNo = string.IsNullOrWhiteSpace(orderNo) ? null : orderNo!.Trim();
  292. ISugarQueryable<AdoSmartOpsKpiDimensionValueDay> Build() =>
  293. _db.Queryable<AdoSmartOpsKpiDimensionValueDay>().ClearFilter<ITenantIdFilter>()
  294. .Where(x => x.TenantId == tenantId && x.MetricCode == metricCode
  295. && x.DimensionConfigVersion == active.DimensionConfigVersion)
  296. .WhereIF(sd.HasValue, x => x.ValueDate >= sd!.Value)
  297. .WhereIF(ed.HasValue, x => x.ValueDate <= ed!.Value)
  298. .WhereIF(!string.IsNullOrWhiteSpace(dimensionType), x => x.DimensionType == dimensionType)
  299. .WhereIF(!string.IsNullOrWhiteSpace(dimensionCode), x => x.DimensionCode == dimensionCode)
  300. .WhereIF(ordNo != null, x => x.OrderNo == ordNo || x.DimensionCode == ordNo);
  301. result.AggregateValue = active.AggregationType switch
  302. {
  303. "AVERAGE_OF_SUMS" => Div(await Build().SumAsync(x => x.SumValue), (decimal?)await Build().SumAsync(x => x.SampleCount)),
  304. "RATIO_OF_SUMS" => Div(await Build().SumAsync(x => x.Numerator), await Build().SumAsync(x => x.Denominator)),
  305. "DIRECT_VALUE" => await Build().AvgAsync(x => x.MetricValue),
  306. _ => null,
  307. };
  308. // 排序字段白名单,禁止任意列注入
  309. var ot = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? OrderByType.Asc : OrderByType.Desc;
  310. var sorted = (sortField ?? "").Trim().ToLowerInvariant() switch
  311. {
  312. "metric_value" or "metricvalue" => Build().OrderBy(x => x.MetricValue, ot).OrderBy(x => x.DimensionCode),
  313. _ => Build().OrderBy(x => x.ValueDate, ot).OrderBy(x => x.DimensionCode),
  314. };
  315. RefAsync<int> total = 0;
  316. var list = await sorted
  317. .ToPageListAsync(page <= 0 ? 1 : page, pageSize <= 0 ? 20 : pageSize, total);
  318. result.Total = total.Value;
  319. result.List = list.Select(x => new KpiDimensionDetailRowDto
  320. {
  321. ValueDate = x.ValueDate.ToString("yyyy-MM-dd"),
  322. DimensionType = x.DimensionType,
  323. DimensionCode = x.DimensionCode,
  324. DimensionName = x.DimensionName,
  325. MetricValue = x.MetricValue,
  326. Numerator = x.Numerator,
  327. Denominator = x.Denominator,
  328. SumValue = x.SumValue,
  329. SampleCount = x.SampleCount,
  330. SourceKey = x.SourceKey,
  331. BatchId = x.BatchId,
  332. }).ToList();
  333. return result;
  334. }
  335. private static decimal? Div(decimal? a, decimal? b) =>
  336. b.HasValue && b.Value != 0 ? (a ?? 0m) / b.Value : null;
  337. /// <summary>已登记数据源列表(第一版仅本地中台库)。</summary>
  338. public List<object> ListDataSources() => new()
  339. {
  340. new { code = KpiDimensionSqlExecutor.LocalDataSourceCode, name = "本地中台库(aidopdev / mdp_std_* / dwd_*)", readOnly = true },
  341. };
  342. internal static KpiSqlRunParams BuildRunParams(long tenantId, string moduleCode, string metricCode, DateTime bizDate) => new()
  343. {
  344. TenantId = tenantId,
  345. FactoryId = 1,
  346. ModuleCode = moduleCode,
  347. MetricCode = metricCode,
  348. BizDate = bizDate,
  349. PeriodStart = bizDate,
  350. PeriodEnd = bizDate.AddDays(1).AddSeconds(-1),
  351. SourceZtid = "pbxfxp",
  352. };
  353. private static readonly string[] KnownDimensionColumns =
  354. {
  355. "value_date", "org_id", "factory_id", "material_code", "work_order_no", "category_code",
  356. "warehouse_code", "order_no", "customer_code", "product_code", "equipment_code",
  357. };
  358. private static List<string> DetectSupportedDimensions(List<string> outputColumns)
  359. {
  360. var set = new HashSet<string>(outputColumns, StringComparer.OrdinalIgnoreCase);
  361. return KnownDimensionColumns.Where(set.Contains).ToList();
  362. }
  363. private static List<string> ParseDimensions(string? json)
  364. {
  365. if (string.IsNullOrWhiteSpace(json)) return new();
  366. try { return System.Text.Json.JsonSerializer.Deserialize<List<string>>(json) ?? new(); }
  367. catch { return new(); }
  368. }
  369. private static Dictionary<string, object?> ToSampleRow(KpiDimensionRow r) => new()
  370. {
  371. ["value_date"] = r.ValueDate == DateTime.MinValue ? null : r.ValueDate.ToString("yyyy-MM-dd"),
  372. ["dimension_type"] = r.DimensionType,
  373. ["dimension_code"] = r.DimensionCode,
  374. ["dimension_name"] = r.DimensionName,
  375. ["metric_value"] = r.MetricValue,
  376. ["numerator"] = r.Numerator,
  377. ["denominator"] = r.Denominator,
  378. ["sum_value"] = r.SumValue,
  379. ["sample_count"] = r.SampleCount,
  380. };
  381. private static void ValidateAggregation(string agg)
  382. {
  383. if (!AggregationTypes.Contains(agg))
  384. throw Oops.Bah($"非法聚合类型:{agg}(DIRECT_VALUE/RATIO_OF_SUMS/AVERAGE_OF_SUMS/SUMMARY_ONLY)");
  385. }
  386. private static int NormalizeTimeout(int t) =>
  387. t <= 0 ? 60 : Math.Min(t, KpiDimensionSqlExecutor.SystemMaxTimeoutSeconds);
  388. private static KpiDimensionConfigDto ToDto(AdoSmartOpsKpiDimensionConfig e) => new()
  389. {
  390. Id = e.Id, TenantId = e.TenantId, MetricCode = e.MetricCode, ModuleCode = e.ModuleCode,
  391. SummaryConfigId = e.SummaryConfigId, SummaryConfigVersion = e.SummaryConfigVersion,
  392. DimensionConfigVersion = e.DimensionConfigVersion, DataSourceCode = e.DataSourceCode,
  393. SqlScript = e.SqlScript, AggregationType = e.AggregationType,
  394. SupportedDimensionsJson = e.SupportedDimensionsJson, OutputContractJson = e.OutputContractJson,
  395. ParameterContractJson = e.ParameterContractJson, TimeoutSeconds = e.TimeoutSeconds,
  396. PublishStatus = e.PublishStatus, IsCurrent = e.IsCurrent, BusinessSqlSource = e.BusinessSqlSource,
  397. ChangeRemark = e.ChangeRemark, Remark = e.Remark, CreatedBy = e.CreatedBy, CreatedAt = e.CreatedAt,
  398. PublishedBy = e.PublishedBy, PublishedAt = e.PublishedAt,
  399. };
  400. }