|
|
@@ -34,6 +34,36 @@ public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
|
|
|
private ISugarQueryable<AdoSmartOpsKpiDimensionConfig> Query() =>
|
|
|
_db.Queryable<AdoSmartOpsKpiDimensionConfig>().ClearFilter<ITenantIdFilter>();
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 写路径一律走原生参数化 SQL(表名与实体 SugarTable 一致,列名 PascalCase:EnableUnderLine=false)。
|
|
|
+ /// 原因:全局 MoreSettings.IsAutoUpdateQueryFilter / IsAutoDeleteQueryFilter=true 会给 ORM 的 UPDATE/DELETE
|
|
|
+ /// 自动追加「登录 JWT 租户」条件,而本表按 ResolveKpiTenantId 落在 KPI 数据租户,
|
|
|
+ /// 两者不一致时静默 0 行(同 UAT-S0-07 / UAT-S9-01 缺陷);SqlSugar 5.1.4 的 IUpdateable/IDeleteable
|
|
|
+ /// 又无法关闭该过滤器(EnableQueryFilter 只能开不能关,且 ClearFilter 仅存在于 ISugarQueryable)。
|
|
|
+ /// 每条写语句都必须显式带 TenantId + 业务状态条件,**禁止按 Id 裸写**。
|
|
|
+ /// </summary>
|
|
|
+ private const string TableName = "ado_smart_ops_kpi_dimension_config";
|
|
|
+
|
|
|
+ /// <summary>取配置归属租户;缺失即数据异常,直接拒绝,避免降级成按 Id 跨租户裸写。</summary>
|
|
|
+ private static long RequireTenantId(AdoSmartOpsKpiDimensionConfig e) =>
|
|
|
+ e.TenantId ?? throw Oops.Bah($"维度配置版本 Id={e.Id} 缺少 TenantId,数据异常,拒绝写入");
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 0 行写入归因:区分「目标不存在 / 租户不匹配 / 状态不满足 / 并发状态变化」,绝不静默返回成功。
|
|
|
+ /// </summary>
|
|
|
+ private async Task<Exception> ExplainZeroRowAsync(
|
|
|
+ long id, long? expectedTenantId, string action, string statusRequirement, params string[] acceptableStatuses)
|
|
|
+ {
|
|
|
+ var now = await Query().Where(x => x.Id == id).FirstAsync();
|
|
|
+ if (now == null)
|
|
|
+ return Oops.Bah($"{action}失败:维度配置版本 Id={id} 不存在(可能已被并发删除)");
|
|
|
+ if (now.TenantId != expectedTenantId)
|
|
|
+ return Oops.Bah($"{action}失败:维度配置版本 Id={id} 归属租户 {now.TenantId},与本次作用域租户 {expectedTenantId} 不一致");
|
|
|
+ if (acceptableStatuses.Length > 0 && !acceptableStatuses.Contains(now.PublishStatus))
|
|
|
+ return Oops.Bah($"{action}失败:维度配置版本 Id={id} 当前状态为 {now.PublishStatus},不满足要求的 {statusRequirement}(并发修改,请刷新后重试)");
|
|
|
+ return Oops.Bah($"{action}失败:维度配置版本 Id={id}(租户 {now.TenantId} / 状态 {now.PublishStatus})未命中任何行,疑似并发状态变化,请刷新后重试");
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>某 KPI 的全部维度配置版本(按版本号倒序)。</summary>
|
|
|
public async Task<List<KpiDimensionConfigDto>> GetByMetricAsync(string metricCode, string moduleCode)
|
|
|
{
|
|
|
@@ -102,19 +132,31 @@ public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
|
|
|
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();
|
|
|
+
|
|
|
+ var tenantId = RequireTenantId(entity);
|
|
|
+ var affected = await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $@"UPDATE {TableName}
|
|
|
+ SET DataSourceCode=@ds, SqlScript=@sql, AggregationType=@agg,
|
|
|
+ SupportedDimensionsJson=@dims, OutputContractJson=@output, ParameterContractJson=@paramContract,
|
|
|
+ TimeoutSeconds=@timeout, BusinessSqlSource=@bizSrc, ChangeRemark=@chg, Remark=@remark,
|
|
|
+ UpdatedBy=@op, UpdatedAt=@now
|
|
|
+ WHERE Id=@id AND TenantId=@tenant AND PublishStatus=@draft",
|
|
|
+ new SugarParameter("@ds", dto.DataSourceCode),
|
|
|
+ new SugarParameter("@sql", dto.SqlScript),
|
|
|
+ new SugarParameter("@agg", dto.AggregationType),
|
|
|
+ new SugarParameter("@dims", dto.SupportedDimensionsJson),
|
|
|
+ new SugarParameter("@output", dto.OutputContractJson),
|
|
|
+ new SugarParameter("@paramContract", dto.ParameterContractJson),
|
|
|
+ new SugarParameter("@timeout", NormalizeTimeout(dto.TimeoutSeconds)),
|
|
|
+ new SugarParameter("@bizSrc", dto.BusinessSqlSource),
|
|
|
+ new SugarParameter("@chg", dto.ChangeRemark),
|
|
|
+ new SugarParameter("@remark", dto.Remark),
|
|
|
+ new SugarParameter("@op", operatorName),
|
|
|
+ new SugarParameter("@now", DateTime.Now),
|
|
|
+ new SugarParameter("@id", id),
|
|
|
+ new SugarParameter("@tenant", tenantId),
|
|
|
+ new SugarParameter("@draft", StatusDraft));
|
|
|
+ if (affected <= 0) throw await ExplainZeroRowAsync(id, tenantId, "编辑草稿", "DRAFT", StatusDraft);
|
|
|
}
|
|
|
|
|
|
/// <summary>删除草稿(仅 DRAFT)。</summary>
|
|
|
@@ -124,7 +166,14 @@ public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
|
|
|
?? throw Oops.Bah("维度配置版本不存在");
|
|
|
if (entity.PublishStatus != StatusDraft)
|
|
|
throw Oops.Bah("仅草稿(DRAFT)可删除;已发布版本请用停用");
|
|
|
- await _db.Deleteable<AdoSmartOpsKpiDimensionConfig>().Where(x => x.Id == id).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ var tenantId = RequireTenantId(entity);
|
|
|
+ var affected = await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $"DELETE FROM {TableName} WHERE Id=@id AND TenantId=@tenant AND PublishStatus=@draft",
|
|
|
+ new SugarParameter("@id", id),
|
|
|
+ new SugarParameter("@tenant", tenantId),
|
|
|
+ new SugarParameter("@draft", StatusDraft));
|
|
|
+ if (affected <= 0) throw await ExplainZeroRowAsync(id, tenantId, "删除草稿", "DRAFT", StatusDraft);
|
|
|
}
|
|
|
|
|
|
/// <summary>SQL 安全校验(复用汇总配置的多层非正则校验器)。</summary>
|
|
|
@@ -207,21 +256,28 @@ public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
|
|
|
if (summary.Id != entity.SummaryConfigId || summary.VersionNo != entity.SummaryConfigVersion)
|
|
|
throw Oops.Bah($"汇总配置已切换到新版本(v{summary.VersionNo}),本维度配置绑定的是 v{entity.SummaryConfigVersion},请基于当前汇总版本新建维度配置");
|
|
|
|
|
|
- var tenantId = entity.TenantId;
|
|
|
+ var tenantId = RequireTenantId(entity);
|
|
|
var tran = await _db.AsTenant().UseTranAsync(async () =>
|
|
|
{
|
|
|
- await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
|
|
|
- .SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = false })
|
|
|
- .Where(x => x.MetricCode == entity.MetricCode && x.TenantId == tenantId && x.Id != id && x.IsCurrent)
|
|
|
- .ExecuteCommandAsync();
|
|
|
- await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
|
|
|
- .SetColumns(x => new AdoSmartOpsKpiDimensionConfig
|
|
|
- {
|
|
|
- PublishStatus = StatusPublished, IsCurrent = true,
|
|
|
- PublishedBy = operatorName, PublishedAt = DateTime.Now,
|
|
|
- })
|
|
|
- .Where(x => x.Id == id)
|
|
|
- .ExecuteCommandAsync();
|
|
|
+ // 旧 current 置 0:0 行是合法结果(本 KPI 此前无生效维度版本),故不做 affected 断言
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $@"UPDATE {TableName} SET IsCurrent=0
|
|
|
+ WHERE MetricCode=@metric AND TenantId=@tenant AND Id<>@id AND IsCurrent=1",
|
|
|
+ new SugarParameter("@metric", entity.MetricCode),
|
|
|
+ new SugarParameter("@tenant", tenantId),
|
|
|
+ new SugarParameter("@id", id));
|
|
|
+ var affected = await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $@"UPDATE {TableName}
|
|
|
+ SET PublishStatus=@published, IsCurrent=1, PublishedBy=@op, PublishedAt=@now
|
|
|
+ WHERE Id=@id AND TenantId=@tenant AND PublishStatus<>@retired",
|
|
|
+ new SugarParameter("@published", StatusPublished),
|
|
|
+ new SugarParameter("@op", operatorName),
|
|
|
+ new SugarParameter("@now", DateTime.Now),
|
|
|
+ new SugarParameter("@id", id),
|
|
|
+ new SugarParameter("@tenant", tenantId),
|
|
|
+ new SugarParameter("@retired", StatusRetired));
|
|
|
+ if (affected <= 0)
|
|
|
+ throw await ExplainZeroRowAsync(id, tenantId, "发布", "非 RETIRED", StatusDraft, StatusPublished);
|
|
|
});
|
|
|
if (!tran.IsSuccess) throw tran.ErrorException;
|
|
|
}
|
|
|
@@ -234,17 +290,26 @@ public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
|
|
|
if (entity.PublishStatus != StatusPublished)
|
|
|
throw Oops.Bah("只能激活已发布(PUBLISHED)版本");
|
|
|
|
|
|
- var tenantId = entity.TenantId;
|
|
|
+ var tenantId = RequireTenantId(entity);
|
|
|
var tran = await _db.AsTenant().UseTranAsync(async () =>
|
|
|
{
|
|
|
- await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
|
|
|
- .SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = false })
|
|
|
- .Where(x => x.MetricCode == entity.MetricCode && x.TenantId == tenantId && x.Id != id && x.IsCurrent)
|
|
|
- .ExecuteCommandAsync();
|
|
|
- await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
|
|
|
- .SetColumns(x => new AdoSmartOpsKpiDimensionConfig { IsCurrent = true, UpdatedBy = operatorName, UpdatedAt = DateTime.Now })
|
|
|
- .Where(x => x.Id == id)
|
|
|
- .ExecuteCommandAsync();
|
|
|
+ // 旧 current 置 0:0 行是合法结果(本 KPI 此前无生效维度版本),故不做 affected 断言
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $@"UPDATE {TableName} SET IsCurrent=0
|
|
|
+ WHERE MetricCode=@metric AND TenantId=@tenant AND Id<>@id AND IsCurrent=1",
|
|
|
+ new SugarParameter("@metric", entity.MetricCode),
|
|
|
+ new SugarParameter("@tenant", tenantId),
|
|
|
+ new SugarParameter("@id", id));
|
|
|
+ var affected = await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $@"UPDATE {TableName} SET IsCurrent=1, UpdatedBy=@op, UpdatedAt=@now
|
|
|
+ WHERE Id=@id AND TenantId=@tenant AND PublishStatus=@published",
|
|
|
+ new SugarParameter("@op", operatorName),
|
|
|
+ new SugarParameter("@now", DateTime.Now),
|
|
|
+ new SugarParameter("@id", id),
|
|
|
+ new SugarParameter("@tenant", tenantId),
|
|
|
+ new SugarParameter("@published", StatusPublished));
|
|
|
+ if (affected <= 0)
|
|
|
+ throw await ExplainZeroRowAsync(id, tenantId, "激活", "PUBLISHED", StatusPublished);
|
|
|
});
|
|
|
if (!tran.IsSuccess) throw tran.ErrorException;
|
|
|
}
|
|
|
@@ -254,14 +319,18 @@ public sealed class AdoSmartOpsKpiDimensionConfigService : ITransient
|
|
|
{
|
|
|
var entity = await Query().Where(x => x.Id == id).FirstAsync()
|
|
|
?? throw Oops.Bah("维度配置版本不存在");
|
|
|
- await _db.Updateable<AdoSmartOpsKpiDimensionConfig>()
|
|
|
- .SetColumns(x => new AdoSmartOpsKpiDimensionConfig
|
|
|
- {
|
|
|
- PublishStatus = StatusRetired, IsCurrent = false,
|
|
|
- RetiredBy = operatorName, RetiredAt = DateTime.Now,
|
|
|
- })
|
|
|
- .Where(x => x.Id == id)
|
|
|
- .ExecuteCommandAsync();
|
|
|
+
|
|
|
+ var tenantId = RequireTenantId(entity);
|
|
|
+ var affected = await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $@"UPDATE {TableName}
|
|
|
+ SET PublishStatus=@retired, IsCurrent=0, RetiredBy=@op, RetiredAt=@now
|
|
|
+ WHERE Id=@id AND TenantId=@tenant",
|
|
|
+ new SugarParameter("@retired", StatusRetired),
|
|
|
+ new SugarParameter("@op", operatorName),
|
|
|
+ new SugarParameter("@now", DateTime.Now),
|
|
|
+ new SugarParameter("@id", id),
|
|
|
+ new SugarParameter("@tenant", tenantId));
|
|
|
+ if (affected <= 0) throw await ExplainZeroRowAsync(id, tenantId, "停用", "任意状态");
|
|
|
}
|
|
|
|
|
|
/// <summary>KPI 维度能力:无生效维度配置 → PENDING_DIMENSION_SQL;SUMMARY_ONLY → SUMMARY_ONLY;否则 CONFIGURED。</summary>
|