MdpSourceScopeFactory.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 包含租户、数据源主键和配置更新时间。连接只看 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. /// 使用调用方已经按租户校验过的数据源,避免相同 source_code 在不同租户间串用连接。
  39. /// </summary>
  40. public Task<ISqlSugarClient> GetScopeAsync(MdpSource source, CancellationToken cancellationToken = default)
  41. {
  42. ArgumentNullException.ThrowIfNull(source);
  43. cancellationToken.ThrowIfCancellationRequested();
  44. return Task.FromResult(OpenScope(source));
  45. }
  46. /// <summary>
  47. /// 按 system_code 取唯一一条启用的 DB 源。0 条或多条都抛错,禁止取第一条。
  48. /// </summary>
  49. public async Task<ISqlSugarClient> GetScopeBySystemCodeAsync(string systemCode, CancellationToken cancellationToken = default)
  50. {
  51. if (string.IsNullOrWhiteSpace(systemCode))
  52. throw new ArgumentException("systemCode 不能为空", nameof(systemCode));
  53. var code = systemCode.Trim();
  54. var all = await _db.Queryable<MdpSource>()
  55. .Where(x => x.Status == 1 && x.SourceType == "DB")
  56. .ToListAsync(cancellationToken);
  57. var rows = all
  58. .Where(x => string.Equals(
  59. string.IsNullOrWhiteSpace(x.SystemCode) ? x.SourceCode : x.SystemCode,
  60. code,
  61. StringComparison.OrdinalIgnoreCase))
  62. .ToList();
  63. if (rows.Count == 0)
  64. throw new InvalidOperationException($"mdp_source 未找到启用的 DB 源:system_code={code}");
  65. if (rows.Count > 1)
  66. throw new InvalidOperationException(
  67. $"system_code={code} 命中多条 DB 源:{string.Join(",", rows.Select(x => x.SourceCode))}。请改用 GetScopeAsync(sourceCode)。");
  68. return OpenScope(rows[0]);
  69. }
  70. private ISqlSugarClient OpenScope(MdpSource source)
  71. {
  72. if (!string.Equals(source.SourceType, "DB", StringComparison.OrdinalIgnoreCase))
  73. throw new InvalidOperationException($"源 {source.SourceCode} 的 source_type={source.SourceType},不是 DB,无法建 SqlSugar 连接");
  74. var mode = (source.ConnMode ?? "").Trim().ToUpperInvariant();
  75. if (mode == "SELF")
  76. return _db;
  77. if (mode != "EXTERNAL")
  78. throw new InvalidOperationException($"源 {source.SourceCode} 的 conn_mode='{source.ConnMode}' 无法解析。只接受 SELF / EXTERNAL。");
  79. if (string.IsNullOrWhiteSpace(source.DbHost) || string.IsNullOrWhiteSpace(source.DbName) || string.IsNullOrWhiteSpace(source.DbUser))
  80. throw new InvalidOperationException($"源 {source.SourceCode} 是 EXTERNAL,但缺少 db_host / db_name / db_user。请在数据源页面补齐,禁止回退主库。");
  81. // 更新时间进入 ConfigId:页面修改主机、账号或密码后立即使用新连接,
  82. // 不复用本进程里按旧配置创建的 SqlSugar 连接域。
  83. var configId = $"{ConfigIdPrefix}{source.TenantId}-{source.Id}-{source.UpdateTime.Ticks}";
  84. if (_db.AsTenant().IsAnyConnection(configId))
  85. return _db.AsTenant().GetConnectionScope(configId);
  86. var connStr = BuildConnectionString(source);
  87. var dbType = MapDbType(source.DbType);
  88. _db.AsTenant().AddConnection(new ConnectionConfig
  89. {
  90. ConfigId = configId,
  91. DbType = dbType,
  92. ConnectionString = connStr,
  93. InitKeyType = InitKeyType.Attribute,
  94. IsAutoCloseConnection = true,
  95. MoreSettings = new ConnMoreSettings
  96. {
  97. IsAutoRemoveDataCache = true
  98. }
  99. });
  100. return _db.AsTenant().GetConnectionScope(configId);
  101. }
  102. public ISqlSugarClient GetScope(string sourceCode) =>
  103. GetScopeAsync(sourceCode).GetAwaiter().GetResult();
  104. private static DbType MapDbType(string? dbType)
  105. {
  106. if (string.IsNullOrWhiteSpace(dbType)) return DbType.MySql;
  107. return dbType.Trim().ToUpperInvariant() switch
  108. {
  109. "MYSQL" => DbType.MySql,
  110. "SQLSERVER" or "MSSQL" => DbType.SqlServer,
  111. "ORACLE" => DbType.Oracle,
  112. "POSTGRES" or "POSTGRESQL" => DbType.PostgreSQL,
  113. _ => throw new NotSupportedException($"不支持的 db_type:{dbType}")
  114. };
  115. }
  116. private string BuildConnectionString(MdpSource source)
  117. {
  118. if (string.IsNullOrWhiteSpace(source.DbHost) || string.IsNullOrWhiteSpace(source.DbName))
  119. throw new InvalidOperationException($"源 {source.SourceCode} 缺少 db_host/db_name");
  120. var password = _passwords.Resolve(source.SourceCode, source.DbPasswordEnc);
  121. var port = source.DbPort;
  122. var user = source.DbUser ?? "";
  123. var dbType = MapDbType(source.DbType);
  124. var extraBody = SanitizeExtraParams(source.DbExtraParams, dbType, out var dropped);
  125. if (dropped.Count > 0)
  126. _logger?.LogWarning(
  127. "源 {SourceCode} 的 db_extra_params 含非法关键字 {Keys},已忽略",
  128. source.SourceCode, string.Join(",", dropped));
  129. var extra = string.IsNullOrEmpty(extraBody) ? "" : ";" + extraBody;
  130. return dbType switch
  131. {
  132. DbType.SqlServer =>
  133. $"Server={source.DbHost}{(port is > 0 ? $",{port}" : "")};Database={source.DbName};User Id={user};Password={password};TrustServerCertificate=true;Encrypt=false{extra}",
  134. DbType.MySql =>
  135. $"Server={source.DbHost};Port={(port is > 0 ? port : 3306)};Database={source.DbName};Uid={user};Pwd={password};CharSet=utf8mb4;AllowLoadLocalInfile=true{extra}",
  136. DbType.PostgreSQL =>
  137. $"Host={source.DbHost};Port={(port is > 0 ? port : 5432)};Database={source.DbName};Username={user};Password={password}{extra}",
  138. DbType.Oracle =>
  139. $"Data Source={source.DbHost}:{(port is > 0 ? port : 1521)}/{source.DbName};User Id={user};Password={password}{extra}",
  140. _ => throw new NotSupportedException($"不支持的 db_type:{source.DbType}")
  141. };
  142. }
  143. /// <summary>
  144. /// 剔掉不属于连接串的键:SqlSugar 的 ConfigId,以及本方法已经写死的键。
  145. /// 按数据库类型分集合,避免一份黑名单误伤 MySQL / PostgreSQL / Oracle 的合法参数。
  146. /// </summary>
  147. internal static IReadOnlyList<string> IllegalExtraKeys(string? dbType, string? extra)
  148. {
  149. SanitizeExtraParams(extra, MapDbType(dbType), out var dropped);
  150. return dropped;
  151. }
  152. internal static string? SanitizeExtraParams(string? extra, DbType dbType, out List<string> dropped)
  153. {
  154. dropped = new List<string>();
  155. if (string.IsNullOrWhiteSpace(extra)) return null;
  156. DbConnectionStringBuilder parsed;
  157. try
  158. {
  159. parsed = new DbConnectionStringBuilder { ConnectionString = extra.Trim().TrimStart(';') };
  160. }
  161. catch (ArgumentException)
  162. {
  163. dropped.Add("(unparseable)");
  164. return null;
  165. }
  166. var reserved = ReservedConnectionKeys(dbType);
  167. var kept = new DbConnectionStringBuilder();
  168. foreach (string key in parsed.Keys)
  169. {
  170. if (IsSqlSugarOnlyKey(key) || reserved.Contains(key))
  171. dropped.Add(key);
  172. else
  173. kept[key] = parsed[key];
  174. }
  175. return kept.Count == 0 ? null : kept.ConnectionString;
  176. }
  177. internal static string? SanitizeStoredExtra(string? dbType, string? extra)
  178. {
  179. if (string.IsNullOrWhiteSpace(extra)) return null;
  180. return SanitizeExtraParams(extra, MapDbType(dbType), out _);
  181. }
  182. private static bool IsSqlSugarOnlyKey(string key) =>
  183. string.Equals(key, "ConfigId", StringComparison.OrdinalIgnoreCase);
  184. private static HashSet<string> ReservedConnectionKeys(DbType dbType) => dbType switch
  185. {
  186. DbType.SqlServer => new(StringComparer.OrdinalIgnoreCase)
  187. {
  188. "Server", "Data Source", "Address", "Addr", "Network Address",
  189. "Database", "Initial Catalog",
  190. "User Id", "UID", "User",
  191. "Password", "PWD",
  192. "TrustServerCertificate", "Encrypt"
  193. },
  194. DbType.MySql => new(StringComparer.OrdinalIgnoreCase)
  195. {
  196. "Server", "Host", "Port", "Database", "Uid", "User Id", "Pwd", "Password",
  197. "CharSet", "AllowLoadLocalInfile"
  198. },
  199. DbType.PostgreSQL => new(StringComparer.OrdinalIgnoreCase)
  200. {
  201. "Host", "Port", "Database", "Username", "User Id", "Password"
  202. },
  203. DbType.Oracle => new(StringComparer.OrdinalIgnoreCase)
  204. {
  205. "Data Source", "User Id", "Password"
  206. },
  207. _ => new(StringComparer.OrdinalIgnoreCase)
  208. };
  209. }