using System.Data.Common; using Admin.NET.Core; using Admin.NET.Plugin.AiDOP.Entity.DataPlatform; using Microsoft.Extensions.Logging; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.DataPlatform; /// /// 按 mdp_source 动态获取只读 SqlSugar 连接域。 /// ConfigId 约定:mdp-src-{sourceCode}。连接只看 conn_mode 与 db_*,不按来源编码分支。 /// public sealed class MdpSourceScopeFactory : ITransient { private const string ConfigIdPrefix = "mdp-src-"; private readonly ISqlSugarClient _db; private readonly ILogger? _logger; public MdpSourceScopeFactory(ISqlSugarClient db, ILogger? logger = null) { _db = db; _logger = logger; } /// 按 sourceCode 获取连接域;优先复用已注册 ConfigId。 public async Task GetScopeAsync(string sourceCode, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(sourceCode)) throw new ArgumentException("sourceCode 不能为空", nameof(sourceCode)); var code = sourceCode.Trim(); var source = await _db.Queryable() .Where(x => x.SourceCode == code && x.Status == 1) .FirstAsync(cancellationToken) ?? throw new InvalidOperationException($"mdp_source 未找到启用源:{code}"); return OpenScope(source); } /// /// 按 system_code 取唯一一条启用的 DB 源。0 条或多条都抛错,禁止取第一条。 /// public async Task GetScopeBySystemCodeAsync(string systemCode, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(systemCode)) throw new ArgumentException("systemCode 不能为空", nameof(systemCode)); var code = systemCode.Trim(); var all = await _db.Queryable() .Where(x => x.Status == 1 && x.SourceType == "DB") .ToListAsync(cancellationToken); var rows = all .Where(x => string.Equals( string.IsNullOrWhiteSpace(x.SystemCode) ? x.SourceCode : x.SystemCode, code, StringComparison.OrdinalIgnoreCase)) .ToList(); if (rows.Count == 0) throw new InvalidOperationException($"mdp_source 未找到启用的 DB 源:system_code={code}"); if (rows.Count > 1) throw new InvalidOperationException( $"system_code={code} 命中多条 DB 源:{string.Join(",", rows.Select(x => x.SourceCode))}。请改用 GetScopeAsync(sourceCode)。"); return OpenScope(rows[0]); } private ISqlSugarClient OpenScope(MdpSource source) { if (!string.Equals(source.SourceType, "DB", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"源 {source.SourceCode} 的 source_type={source.SourceType},不是 DB,无法建 SqlSugar 连接"); var mode = (source.ConnMode ?? "").Trim().ToUpperInvariant(); if (mode == "SELF") return _db; if (mode != "EXTERNAL") throw new InvalidOperationException($"源 {source.SourceCode} 的 conn_mode='{source.ConnMode}' 无法解析。只接受 SELF / EXTERNAL。"); if (string.IsNullOrWhiteSpace(source.DbHost) || string.IsNullOrWhiteSpace(source.DbName) || string.IsNullOrWhiteSpace(source.DbUser)) throw new InvalidOperationException($"源 {source.SourceCode} 是 EXTERNAL,但缺少 db_host / db_name / db_user。请在数据源页面补齐,禁止回退主库。"); var configId = ConfigIdPrefix + source.SourceCode; if (_db.AsTenant().IsAnyConnection(configId)) return _db.AsTenant().GetConnectionScope(configId); var connStr = BuildConnectionString(source); var dbType = MapDbType(source.DbType); _db.AsTenant().AddConnection(new ConnectionConfig { ConfigId = configId, DbType = dbType, ConnectionString = connStr, InitKeyType = InitKeyType.Attribute, IsAutoCloseConnection = true, MoreSettings = new ConnMoreSettings { IsAutoRemoveDataCache = true } }); return _db.AsTenant().GetConnectionScope(configId); } public ISqlSugarClient GetScope(string sourceCode) => GetScopeAsync(sourceCode).GetAwaiter().GetResult(); private static DbType MapDbType(string? dbType) { if (string.IsNullOrWhiteSpace(dbType)) return DbType.MySql; return dbType.Trim().ToUpperInvariant() switch { "MYSQL" => DbType.MySql, "SQLSERVER" or "MSSQL" => DbType.SqlServer, "ORACLE" => DbType.Oracle, "POSTGRES" or "POSTGRESQL" => DbType.PostgreSQL, _ => throw new NotSupportedException($"不支持的 db_type:{dbType}") }; } private string BuildConnectionString(MdpSource source) { if (string.IsNullOrWhiteSpace(source.DbHost) || string.IsNullOrWhiteSpace(source.DbName)) throw new InvalidOperationException($"源 {source.SourceCode} 缺少 db_host/db_name"); var password = DecryptPassword(source.DbPasswordEnc); var port = source.DbPort; var user = source.DbUser ?? ""; var dbType = MapDbType(source.DbType); var extraBody = SanitizeExtraParams(source.DbExtraParams, dbType, out var dropped); if (dropped.Count > 0) _logger?.LogWarning( "源 {SourceCode} 的 db_extra_params 含非法关键字 {Keys},已忽略", source.SourceCode, string.Join(",", dropped)); var extra = string.IsNullOrEmpty(extraBody) ? "" : ";" + extraBody; return dbType switch { DbType.SqlServer => $"Server={source.DbHost}{(port is > 0 ? $",{port}" : "")};Database={source.DbName};User Id={user};Password={password};TrustServerCertificate=true;Encrypt=false{extra}", DbType.MySql => $"Server={source.DbHost};Port={(port is > 0 ? port : 3306)};Database={source.DbName};Uid={user};Pwd={password};CharSet=utf8mb4;AllowLoadLocalInfile=true{extra}", DbType.PostgreSQL => $"Host={source.DbHost};Port={(port is > 0 ? port : 5432)};Database={source.DbName};Username={user};Password={password}{extra}", DbType.Oracle => $"Data Source={source.DbHost}:{(port is > 0 ? port : 1521)}/{source.DbName};User Id={user};Password={password}{extra}", _ => throw new NotSupportedException($"不支持的 db_type:{source.DbType}") }; } /// /// 剔掉不属于连接串的键:SqlSugar 的 ConfigId,以及本方法已经写死的键。 /// 按数据库类型分集合,避免一份黑名单误伤 MySQL / PostgreSQL / Oracle 的合法参数。 /// internal static IReadOnlyList IllegalExtraKeys(string? dbType, string? extra) { SanitizeExtraParams(extra, MapDbType(dbType), out var dropped); return dropped; } internal static string? SanitizeExtraParams(string? extra, DbType dbType, out List dropped) { dropped = new List(); if (string.IsNullOrWhiteSpace(extra)) return null; DbConnectionStringBuilder parsed; try { parsed = new DbConnectionStringBuilder { ConnectionString = extra.Trim().TrimStart(';') }; } catch (ArgumentException) { dropped.Add("(unparseable)"); return null; } var reserved = ReservedConnectionKeys(dbType); var kept = new DbConnectionStringBuilder(); foreach (string key in parsed.Keys) { if (IsSqlSugarOnlyKey(key) || reserved.Contains(key)) dropped.Add(key); else kept[key] = parsed[key]; } return kept.Count == 0 ? null : kept.ConnectionString; } private static bool IsSqlSugarOnlyKey(string key) => string.Equals(key, "ConfigId", StringComparison.OrdinalIgnoreCase); private static HashSet ReservedConnectionKeys(DbType dbType) => dbType switch { DbType.SqlServer => new(StringComparer.OrdinalIgnoreCase) { "Server", "Data Source", "Address", "Addr", "Network Address", "Database", "Initial Catalog", "User Id", "UID", "User", "Password", "PWD", "TrustServerCertificate", "Encrypt" }, DbType.MySql => new(StringComparer.OrdinalIgnoreCase) { "Server", "Host", "Port", "Database", "Uid", "User Id", "Pwd", "Password", "CharSet", "AllowLoadLocalInfile" }, DbType.PostgreSQL => new(StringComparer.OrdinalIgnoreCase) { "Host", "Port", "Database", "Username", "User Id", "Password" }, DbType.Oracle => new(StringComparer.OrdinalIgnoreCase) { "Data Source", "User Id", "Password" }, _ => new(StringComparer.OrdinalIgnoreCase) }; private static string DecryptPassword(string? enc) { if (string.IsNullOrEmpty(enc)) return ""; try { var plain = CryptogramUtil.Decrypt(enc); return string.IsNullOrEmpty(plain) ? enc : plain; } catch { // 明文或非本系统密文时原样使用(与 Database.json EnableConnEncrypt=false 一致) return enc; } } }