namespace Admin.NET.Plugin.AiDOP.Rule;
///
/// 规则配置只读数据源边界。后续数据库实现只需替换该接口实现。
///
public interface IRuleConfigRepository
{
Task> GetEffectiveLayersAsync(
string moduleCode,
string scenarioCode,
long tenantId,
long? factoryId,
CancellationToken cancellationToken = default);
}
///
/// 默认空数据源,主要用于单元测试或手动回退。
///
public sealed class EmptyRuleConfigRepository : IRuleConfigRepository
{
public Task> GetEffectiveLayersAsync(
string moduleCode,
string scenarioCode,
long tenantId,
long? factoryId,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
_ = moduleCode;
_ = scenarioCode;
_ = tenantId;
_ = factoryId;
return Task.FromResult>(Array.Empty());
}
}
///
/// `ado_rule_*` 数据库只读仓储。研发阶段默认启用,用于验证规则配置链路。
///
public sealed class DbRuleConfigRepository : IRuleConfigRepository, ITransient
{
private readonly ISqlSugarClient _db;
public DbRuleConfigRepository(ISqlSugarClient db)
{
_db = db;
}
public async Task> GetEffectiveLayersAsync(
string moduleCode,
string scenarioCode,
long tenantId,
long? factoryId,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var now = DateTime.Now;
var normalizedFactoryId = factoryId.GetValueOrDefault();
var rows = await _db.Ado.SqlQueryAsync(
"""
SELECT
p.id AS ProfileId,
p.tenant_id AS TenantId,
p.factory_id AS FactoryId,
p.profile_code AS ProfileCode,
i.rule_code AS RuleCode,
i.value_type AS ValueType,
i.rule_value AS RuleValue,
i.is_enabled AS ItemEnabled,
i.sort_no AS SortNo,
s.required AS Required,
s.ui_control AS UiControl
FROM ado_rule_profile p
INNER JOIN ado_rule_item i ON i.profile_id = p.id
LEFT JOIN ado_rule_schema s
ON s.module_code = p.module_code
AND s.scenario_code = p.scenario_code
AND s.rule_code = i.rule_code
AND s.is_enabled = 1
WHERE p.module_code = @ModuleCode
AND p.scenario_code = @ScenarioCode
AND p.is_enabled = 1
AND i.is_enabled = 1
AND (p.effective_from IS NULL OR p.effective_from <= @Now)
AND (p.effective_to IS NULL OR p.effective_to >= @Now)
AND (
(p.tenant_id = 0 AND p.factory_id = 0)
OR (p.tenant_id = @TenantId AND p.factory_id = 0)
OR (p.tenant_id = @TenantId AND p.factory_id = @FactoryId)
)
ORDER BY p.tenant_id, p.factory_id, p.version, i.sort_no, i.id
""",
new SugarParameter("@ModuleCode", moduleCode),
new SugarParameter("@ScenarioCode", scenarioCode),
new SugarParameter("@TenantId", tenantId),
new SugarParameter("@FactoryId", normalizedFactoryId),
new SugarParameter("@Now", now));
return rows
.GroupBy(x => new { x.ProfileId, x.TenantId, x.FactoryId, x.ProfileCode })
.Select(x => new RuleConfigLayer
{
Scope = BuildScope(x.Key.ProfileCode, x.Key.TenantId, x.Key.FactoryId),
Priority = BuildPriority(x.Key.TenantId, x.Key.FactoryId),
Items = x
.OrderBy(item => item.SortNo)
.Select(item => new RuleConfigItem
{
RuleCode = item.RuleCode,
ValueType = item.ValueType,
RuleValue = item.RuleValue,
Required = item.Required == 1,
IsEnabled = item.ItemEnabled == 1,
IsHighRiskSwitch = false
})
.ToList()
})
.OrderBy(x => x.Priority)
.ToList();
}
private static int BuildPriority(long tenantId, long factoryId)
{
if (tenantId > 0 && factoryId > 0) return 300;
if (tenantId > 0) return 200;
return 100;
}
private static string BuildScope(string profileCode, long tenantId, long factoryId)
{
if (tenantId > 0 && factoryId > 0) return $"factory:{tenantId}:{factoryId}:{profileCode}";
if (tenantId > 0) return $"tenant:{tenantId}:{profileCode}";
return $"global:{profileCode}";
}
private sealed class RuleConfigDbRow
{
public long ProfileId { get; set; }
public long TenantId { get; set; }
public long FactoryId { get; set; }
public string ProfileCode { get; set; } = string.Empty;
public string RuleCode { get; set; } = string.Empty;
public string ValueType { get; set; } = string.Empty;
public string? RuleValue { get; set; }
public int ItemEnabled { get; set; }
public int SortNo { get; set; }
public int? Required { get; set; }
public string? UiControl { get; set; }
}
}