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; /// /// KPI 业务来源与准备状态服务(KPI 级)。缺行时返回 IsPersisted=false 的默认 PENDING_BUSINESS_FIELDS, /// 不隐式建行;置 READY_FOR_CONFIG 时服务端强校验(来源系统/表/字段/SQL来源 + 对应 CONFIG_SQL 草稿含 SqlText/数据源)。 /// ModuleCode / TenantId 一律由 MetricCode 解析,不信任前端(避免错误 ModuleCode 造成重复口径)。 /// 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; } /// 从 MetricCode 解析模块(Sx_Ly_nnn → Sx);非法前缀回落 metricCode 首段大写。 public static string ResolveModuleCode(string metricCode) { var prefix = (metricCode ?? string.Empty).Split('_')[0].Trim().ToUpperInvariant(); return ModuleRe.IsMatch(prefix) ? prefix : prefix; } private ISugarQueryable Query() => _db.Queryable().ClearFilter(); /// 取业务来源登记;库中无记录时返回默认 PENDING_BUSINESS_FIELDS(IsPersisted=false),不建行。 public async Task 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); } /// 门禁用:取业务准备状态;无记录返回 PENDING_BUSINESS_FIELDS。 public async Task 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; } /// 保存业务来源登记(upsert)。置 READY 前强校验;ModuleCode/TenantId 后端解析。 public async Task 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); } /// READY_FOR_CONFIG 前置:业务字段齐 + 存在含 SqlText/数据源的 CONFIG_SQL 版本。缺项明确报出。 private async Task EnsureReadyRequirementsAsync(long tenantId, KpiBusinessInputUpsertDto dto) { var missing = new List(); 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().ClearFilter() .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, }; }