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,
};
}