MdpSourceScopeFactory.cs 9.5 KB

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