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