MdpSourceScopeFactory.cs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. using System.Data.Common;
  2. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  3. using Microsoft.Extensions.Logging;
  4. using SqlSugar;
  5. namespace Admin.NET.Plugin.AiDOP.DataPlatform;
  6. /// <summary>
  7. /// 按 mdp_source 动态获取只读 SqlSugar 连接域。
  8. /// ConfigId 约定:mdp-src-{sourceCode}。连接只看 conn_mode 与 db_*,不按来源编码分支。
  9. /// </summary>
  10. public sealed class MdpSourceScopeFactory : ITransient
  11. {
  12. private const string ConfigIdPrefix = "mdp-src-";
  13. private readonly ISqlSugarClient _db;
  14. private readonly IMdpSourcePasswordResolver _passwords;
  15. private readonly ILogger<MdpSourceScopeFactory>? _logger;
  16. public MdpSourceScopeFactory(
  17. ISqlSugarClient db,
  18. IMdpSourcePasswordResolver passwords,
  19. ILogger<MdpSourceScopeFactory>? logger = null)
  20. {
  21. _db = db;
  22. _passwords = passwords ?? throw new ArgumentNullException(nameof(passwords));
  23. _logger = logger;
  24. }
  25. /// <summary>按 sourceCode 获取连接域;优先复用已注册 ConfigId。</summary>
  26. public async Task<ISqlSugarClient> GetScopeAsync(string sourceCode, CancellationToken cancellationToken = default)
  27. {
  28. if (string.IsNullOrWhiteSpace(sourceCode))
  29. throw new ArgumentException("sourceCode 不能为空", nameof(sourceCode));
  30. var code = sourceCode.Trim();
  31. var source = await _db.Queryable<MdpSource>()
  32. .Where(x => x.SourceCode == code && x.Status == 1)
  33. .FirstAsync(cancellationToken)
  34. ?? throw new InvalidOperationException($"mdp_source 未找到启用源:{code}");
  35. return OpenScope(source);
  36. }
  37. /// <summary>
  38. /// 按 system_code 取唯一一条启用的 DB 源。0 条或多条都抛错,禁止取第一条。
  39. /// </summary>
  40. public async Task<ISqlSugarClient> GetScopeBySystemCodeAsync(string systemCode, CancellationToken cancellationToken = default)
  41. {
  42. if (string.IsNullOrWhiteSpace(systemCode))
  43. throw new ArgumentException("systemCode 不能为空", nameof(systemCode));
  44. var code = systemCode.Trim();
  45. var all = await _db.Queryable<MdpSource>()
  46. .Where(x => x.Status == 1 && x.SourceType == "DB")
  47. .ToListAsync(cancellationToken);
  48. var rows = all
  49. .Where(x => string.Equals(
  50. string.IsNullOrWhiteSpace(x.SystemCode) ? x.SourceCode : x.SystemCode,
  51. code,
  52. StringComparison.OrdinalIgnoreCase))
  53. .ToList();
  54. if (rows.Count == 0)
  55. throw new InvalidOperationException($"mdp_source 未找到启用的 DB 源:system_code={code}");
  56. if (rows.Count > 1)
  57. throw new InvalidOperationException(
  58. $"system_code={code} 命中多条 DB 源:{string.Join(",", rows.Select(x => x.SourceCode))}。请改用 GetScopeAsync(sourceCode)。");
  59. return OpenScope(rows[0]);
  60. }
  61. private ISqlSugarClient OpenScope(MdpSource source)
  62. {
  63. if (!string.Equals(source.SourceType, "DB", StringComparison.OrdinalIgnoreCase))
  64. throw new InvalidOperationException($"源 {source.SourceCode} 的 source_type={source.SourceType},不是 DB,无法建 SqlSugar 连接");
  65. var mode = (source.ConnMode ?? "").Trim().ToUpperInvariant();
  66. if (mode == "SELF")
  67. return _db;
  68. if (mode != "EXTERNAL")
  69. throw new InvalidOperationException($"源 {source.SourceCode} 的 conn_mode='{source.ConnMode}' 无法解析。只接受 SELF / EXTERNAL。");
  70. if (string.IsNullOrWhiteSpace(source.DbHost) || string.IsNullOrWhiteSpace(source.DbName) || string.IsNullOrWhiteSpace(source.DbUser))
  71. throw new InvalidOperationException($"源 {source.SourceCode} 是 EXTERNAL,但缺少 db_host / db_name / db_user。请在数据源页面补齐,禁止回退主库。");
  72. var configId = ConfigIdPrefix + source.SourceCode;
  73. if (_db.AsTenant().IsAnyConnection(configId))
  74. return _db.AsTenant().GetConnectionScope(configId);
  75. var connStr = BuildConnectionString(source);
  76. var dbType = MapDbType(source.DbType);
  77. _db.AsTenant().AddConnection(new ConnectionConfig
  78. {
  79. ConfigId = configId,
  80. DbType = dbType,
  81. ConnectionString = connStr,
  82. InitKeyType = InitKeyType.Attribute,
  83. IsAutoCloseConnection = true,
  84. MoreSettings = new ConnMoreSettings
  85. {
  86. IsAutoRemoveDataCache = true
  87. }
  88. });
  89. return _db.AsTenant().GetConnectionScope(configId);
  90. }
  91. public ISqlSugarClient GetScope(string sourceCode) =>
  92. GetScopeAsync(sourceCode).GetAwaiter().GetResult();
  93. private static DbType MapDbType(string? dbType)
  94. {
  95. if (string.IsNullOrWhiteSpace(dbType)) return DbType.MySql;
  96. return dbType.Trim().ToUpperInvariant() switch
  97. {
  98. "MYSQL" => DbType.MySql,
  99. "SQLSERVER" or "MSSQL" => DbType.SqlServer,
  100. "ORACLE" => DbType.Oracle,
  101. "POSTGRES" or "POSTGRESQL" => DbType.PostgreSQL,
  102. _ => throw new NotSupportedException($"不支持的 db_type:{dbType}")
  103. };
  104. }
  105. private string BuildConnectionString(MdpSource source)
  106. {
  107. if (string.IsNullOrWhiteSpace(source.DbHost) || string.IsNullOrWhiteSpace(source.DbName))
  108. throw new InvalidOperationException($"源 {source.SourceCode} 缺少 db_host/db_name");
  109. var password = _passwords.Resolve(source.SourceCode, source.DbPasswordEnc);
  110. var port = source.DbPort;
  111. var user = source.DbUser ?? "";
  112. var dbType = MapDbType(source.DbType);
  113. var extraBody = SanitizeExtraParams(source.DbExtraParams, dbType, out var dropped);
  114. if (dropped.Count > 0)
  115. _logger?.LogWarning(
  116. "源 {SourceCode} 的 db_extra_params 含非法关键字 {Keys},已忽略",
  117. source.SourceCode, string.Join(",", dropped));
  118. var extra = string.IsNullOrEmpty(extraBody) ? "" : ";" + extraBody;
  119. return dbType switch
  120. {
  121. DbType.SqlServer =>
  122. $"Server={source.DbHost}{(port is > 0 ? $",{port}" : "")};Database={source.DbName};User Id={user};Password={password};TrustServerCertificate=true;Encrypt=false{extra}",
  123. DbType.MySql =>
  124. $"Server={source.DbHost};Port={(port is > 0 ? port : 3306)};Database={source.DbName};Uid={user};Pwd={password};CharSet=utf8mb4;AllowLoadLocalInfile=true{extra}",
  125. DbType.PostgreSQL =>
  126. $"Host={source.DbHost};Port={(port is > 0 ? port : 5432)};Database={source.DbName};Username={user};Password={password}{extra}",
  127. DbType.Oracle =>
  128. $"Data Source={source.DbHost}:{(port is > 0 ? port : 1521)}/{source.DbName};User Id={user};Password={password}{extra}",
  129. _ => throw new NotSupportedException($"不支持的 db_type:{source.DbType}")
  130. };
  131. }
  132. /// <summary>
  133. /// 剔掉不属于连接串的键:SqlSugar 的 ConfigId,以及本方法已经写死的键。
  134. /// 按数据库类型分集合,避免一份黑名单误伤 MySQL / PostgreSQL / Oracle 的合法参数。
  135. /// </summary>
  136. internal static IReadOnlyList<string> IllegalExtraKeys(string? dbType, string? extra)
  137. {
  138. SanitizeExtraParams(extra, MapDbType(dbType), out var dropped);
  139. return dropped;
  140. }
  141. internal static string? SanitizeExtraParams(string? extra, DbType dbType, out List<string> dropped)
  142. {
  143. dropped = new List<string>();
  144. if (string.IsNullOrWhiteSpace(extra)) return null;
  145. DbConnectionStringBuilder parsed;
  146. try
  147. {
  148. parsed = new DbConnectionStringBuilder { ConnectionString = extra.Trim().TrimStart(';') };
  149. }
  150. catch (ArgumentException)
  151. {
  152. dropped.Add("(unparseable)");
  153. return null;
  154. }
  155. var reserved = ReservedConnectionKeys(dbType);
  156. var kept = new DbConnectionStringBuilder();
  157. foreach (string key in parsed.Keys)
  158. {
  159. if (IsSqlSugarOnlyKey(key) || reserved.Contains(key))
  160. dropped.Add(key);
  161. else
  162. kept[key] = parsed[key];
  163. }
  164. return kept.Count == 0 ? null : kept.ConnectionString;
  165. }
  166. private static bool IsSqlSugarOnlyKey(string key) =>
  167. string.Equals(key, "ConfigId", StringComparison.OrdinalIgnoreCase);
  168. private static HashSet<string> ReservedConnectionKeys(DbType dbType) => dbType switch
  169. {
  170. DbType.SqlServer => new(StringComparer.OrdinalIgnoreCase)
  171. {
  172. "Server", "Data Source", "Address", "Addr", "Network Address",
  173. "Database", "Initial Catalog",
  174. "User Id", "UID", "User",
  175. "Password", "PWD",
  176. "TrustServerCertificate", "Encrypt"
  177. },
  178. DbType.MySql => new(StringComparer.OrdinalIgnoreCase)
  179. {
  180. "Server", "Host", "Port", "Database", "Uid", "User Id", "Pwd", "Password",
  181. "CharSet", "AllowLoadLocalInfile"
  182. },
  183. DbType.PostgreSQL => new(StringComparer.OrdinalIgnoreCase)
  184. {
  185. "Host", "Port", "Database", "Username", "User Id", "Password"
  186. },
  187. DbType.Oracle => new(StringComparer.OrdinalIgnoreCase)
  188. {
  189. "Data Source", "User Id", "Password"
  190. },
  191. _ => new(StringComparer.OrdinalIgnoreCase)
  192. };
  193. }