|
|
@@ -0,0 +1,162 @@
|
|
|
+using System.Text.RegularExpressions;
|
|
|
+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;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// KPI 业务来源与准备状态服务(KPI 级)。缺行时返回 IsPersisted=false 的默认 PENDING_BUSINESS_FIELDS,
|
|
|
+/// 不隐式建行;置 READY_FOR_CONFIG 时服务端强校验(来源系统/表/字段/SQL来源 + 对应 CONFIG_SQL 草稿含 SqlText/数据源)。
|
|
|
+/// ModuleCode / TenantId 一律由 MetricCode 解析,不信任前端(避免错误 ModuleCode 造成重复口径)。
|
|
|
+/// </summary>
|
|
|
+public sealed class AdoSmartOpsKpiBusinessInputService : ITransient
|
|
|
+{
|
|
|
+ public const string StatusPendingFields = "PENDING_BUSINESS_FIELDS";
|
|
|
+ public const string StatusPendingSql = "PENDING_BUSINESS_SQL";
|
|
|
+ public const string StatusReady = "READY_FOR_CONFIG";
|
|
|
+ private const string EngineConfigSql = "CONFIG_SQL";
|
|
|
+ private static readonly string[] AllStatuses = { StatusPendingFields, StatusPendingSql, StatusReady };
|
|
|
+ private static readonly Regex ModuleRe = new(@"^S[1-9]$", RegexOptions.Compiled);
|
|
|
+
|
|
|
+ private readonly ISqlSugarClient _db;
|
|
|
+
|
|
|
+ public AdoSmartOpsKpiBusinessInputService(ISqlSugarClient db)
|
|
|
+ {
|
|
|
+ _db = db;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>从 MetricCode 解析模块(Sx_Ly_nnn → Sx);非法前缀回落 metricCode 首段大写。</summary>
|
|
|
+ public static string ResolveModuleCode(string metricCode)
|
|
|
+ {
|
|
|
+ var prefix = (metricCode ?? string.Empty).Split('_')[0].Trim().ToUpperInvariant();
|
|
|
+ return ModuleRe.IsMatch(prefix) ? prefix : prefix;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ISugarQueryable<AdoSmartOpsKpiBusinessInput> Query() =>
|
|
|
+ _db.Queryable<AdoSmartOpsKpiBusinessInput>().ClearFilter<ITenantIdFilter>();
|
|
|
+
|
|
|
+ /// <summary>取业务来源登记;库中无记录时返回默认 PENDING_BUSINESS_FIELDS(IsPersisted=false),不建行。</summary>
|
|
|
+ public async Task<KpiBusinessInputDto> GetAsync(string metricCode)
|
|
|
+ {
|
|
|
+ var moduleCode = ResolveModuleCode(metricCode);
|
|
|
+ var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
|
|
|
+ var e = await Query().Where(x => x.MetricCode == metricCode && x.TenantId == tenantId).FirstAsync();
|
|
|
+ if (e == null)
|
|
|
+ return new KpiBusinessInputDto
|
|
|
+ {
|
|
|
+ IsPersisted = false,
|
|
|
+ TenantId = tenantId,
|
|
|
+ MetricCode = metricCode,
|
|
|
+ ModuleCode = moduleCode,
|
|
|
+ BusinessInputStatus = StatusPendingFields,
|
|
|
+ };
|
|
|
+ return ToDto(e);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>门禁用:取业务准备状态;无记录返回 PENDING_BUSINESS_FIELDS。</summary>
|
|
|
+ public async Task<string> GetStatusAsync(long tenantId, string metricCode)
|
|
|
+ {
|
|
|
+ var e = await Query().Where(x => x.MetricCode == metricCode && x.TenantId == tenantId).FirstAsync();
|
|
|
+ return string.IsNullOrWhiteSpace(e?.BusinessInputStatus) ? StatusPendingFields : e!.BusinessInputStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>保存业务来源登记(upsert)。置 READY 前强校验;ModuleCode/TenantId 后端解析。</summary>
|
|
|
+ public async Task<KpiBusinessInputDto> UpsertAsync(KpiBusinessInputUpsertDto dto, string operatorName)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.MetricCode))
|
|
|
+ throw Oops.Bah("metricCode 必填");
|
|
|
+ var status = (dto.BusinessInputStatus ?? string.Empty).Trim().ToUpperInvariant();
|
|
|
+ if (!AllStatuses.Contains(status))
|
|
|
+ throw Oops.Bah($"非法业务准备状态:{dto.BusinessInputStatus}(PENDING_BUSINESS_FIELDS/PENDING_BUSINESS_SQL/READY_FOR_CONFIG)");
|
|
|
+
|
|
|
+ var moduleCode = ResolveModuleCode(dto.MetricCode);
|
|
|
+ var tenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode);
|
|
|
+
|
|
|
+ if (status == StatusReady)
|
|
|
+ await EnsureReadyRequirementsAsync(tenantId, dto);
|
|
|
+
|
|
|
+ var existing = await Query().Where(x => x.MetricCode == dto.MetricCode && x.TenantId == tenantId).FirstAsync();
|
|
|
+ var now = DateTime.Now;
|
|
|
+ if (existing == null)
|
|
|
+ {
|
|
|
+ var entity = new AdoSmartOpsKpiBusinessInput
|
|
|
+ {
|
|
|
+ TenantId = tenantId,
|
|
|
+ MetricCode = dto.MetricCode,
|
|
|
+ ModuleCode = moduleCode,
|
|
|
+ BusinessInputStatus = status,
|
|
|
+ SourceSystem = Trim(dto.SourceSystem),
|
|
|
+ SourceTables = Trim(dto.SourceTables),
|
|
|
+ SourceFields = Trim(dto.SourceFields),
|
|
|
+ BusinessSqlSource = Trim(dto.BusinessSqlSource),
|
|
|
+ BusinessSqlRemark = Trim(dto.BusinessSqlRemark),
|
|
|
+ BusinessOwner = Trim(dto.BusinessOwner),
|
|
|
+ FieldOwner = Trim(dto.FieldOwner),
|
|
|
+ Remark = Trim(dto.Remark),
|
|
|
+ CreatedBy = operatorName,
|
|
|
+ CreatedAt = now,
|
|
|
+ };
|
|
|
+ entity.Id = await _db.Insertable(entity).ExecuteReturnBigIdentityAsync();
|
|
|
+ return ToDto(entity);
|
|
|
+ }
|
|
|
+
|
|
|
+ existing.ModuleCode = moduleCode;
|
|
|
+ existing.BusinessInputStatus = status;
|
|
|
+ existing.SourceSystem = Trim(dto.SourceSystem);
|
|
|
+ existing.SourceTables = Trim(dto.SourceTables);
|
|
|
+ existing.SourceFields = Trim(dto.SourceFields);
|
|
|
+ existing.BusinessSqlSource = Trim(dto.BusinessSqlSource);
|
|
|
+ existing.BusinessSqlRemark = Trim(dto.BusinessSqlRemark);
|
|
|
+ existing.BusinessOwner = Trim(dto.BusinessOwner);
|
|
|
+ existing.FieldOwner = Trim(dto.FieldOwner);
|
|
|
+ existing.Remark = Trim(dto.Remark);
|
|
|
+ existing.UpdatedBy = operatorName;
|
|
|
+ existing.UpdatedAt = now;
|
|
|
+ await _db.Updateable(existing).ExecuteCommandAsync();
|
|
|
+ return ToDto(existing);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>READY_FOR_CONFIG 前置:业务字段齐 + 存在含 SqlText/数据源的 CONFIG_SQL 版本。缺项明确报出。</summary>
|
|
|
+ private async Task EnsureReadyRequirementsAsync(long tenantId, KpiBusinessInputUpsertDto dto)
|
|
|
+ {
|
|
|
+ var missing = new List<string>();
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.SourceSystem)) missing.Add("来源系统");
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.SourceTables)) missing.Add("来源表");
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.SourceFields)) missing.Add("来源字段");
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.BusinessSqlSource)) missing.Add("SQL 来源");
|
|
|
+
|
|
|
+ var hasConfigSql = await _db.Queryable<AdoSmartOpsKpiCalcConfig>().ClearFilter<ITenantIdFilter>()
|
|
|
+ .Where(x => x.MetricCode == dto.MetricCode && x.TenantId == tenantId
|
|
|
+ && x.CalcEngineType == EngineConfigSql
|
|
|
+ && x.SqlScript != null && x.SqlScript != ""
|
|
|
+ && x.DataSourceCode != null && x.DataSourceCode != "")
|
|
|
+ .AnyAsync();
|
|
|
+ if (!hasConfigSql) missing.Add("对应 CONFIG_SQL 配置(含 SqlText + 数据源)");
|
|
|
+
|
|
|
+ if (missing.Count > 0)
|
|
|
+ throw Oops.Bah($"不能置为 READY_FOR_CONFIG,尚缺:{string.Join("、", missing)}");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string? Trim(string? s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
|
|
|
+
|
|
|
+ private static KpiBusinessInputDto ToDto(AdoSmartOpsKpiBusinessInput e) => new()
|
|
|
+ {
|
|
|
+ IsPersisted = true,
|
|
|
+ TenantId = e.TenantId,
|
|
|
+ MetricCode = e.MetricCode,
|
|
|
+ ModuleCode = e.ModuleCode,
|
|
|
+ BusinessInputStatus = e.BusinessInputStatus,
|
|
|
+ SourceSystem = e.SourceSystem,
|
|
|
+ SourceTables = e.SourceTables,
|
|
|
+ SourceFields = e.SourceFields,
|
|
|
+ BusinessSqlSource = e.BusinessSqlSource,
|
|
|
+ BusinessSqlRemark = e.BusinessSqlRemark,
|
|
|
+ BusinessOwner = e.BusinessOwner,
|
|
|
+ FieldOwner = e.FieldOwner,
|
|
|
+ Remark = e.Remark,
|
|
|
+ UpdatedBy = e.UpdatedBy,
|
|
|
+ UpdatedAt = e.UpdatedAt,
|
|
|
+ };
|
|
|
+}
|