Forráskód Böngészése

feat(s8): normalize SQL evaluator data source scopes

YY968XX 1 hónapja
szülő
commit
9b627776d5

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.140</AssemblyVersion>
-    <FileVersion>1.0.140</FileVersion>
-    <Version>1.0.140</Version>
+    <AssemblyVersion>1.0.141</AssemblyVersion>
+    <FileVersion>1.0.141</FileVersion>
+    <Version>1.0.141</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 80 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/Rules/S8DataSourceEndpointNormalizer.cs

@@ -0,0 +1,80 @@
+using System.Data.Common;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
+
+/// <summary>
+/// S8-DYNAMIC-SQLSUGAR-SCOPE-FACTORY-1:S8 data_source endpoint 规范化与 hash 派生。
+/// 纯字符串变换:
+///   1) 用 DbConnectionStringBuilder 解析 raw endpoint 的 key/value(大小写无关);
+///   2) 按 OrdinalIgnoreCase 全部 key lower-case 化,避免不同输入 case 导致 hash 漂移;
+///   3) 补齐缺失参数(已有 key 不覆盖),与主库 Database.json 池层参数对齐:
+///        AllowPublicKeyRetrieval / Pooling / Minimum Pool Size / Maximum Pool Size /
+///        Connection Timeout / Connection Idle Timeout / Connection LifeTime;
+///   4) 按 key 字母序输出 normalized canonical connection string;
+///   5) 对 normalized 做 SHA256 → 64 字符小写 hex EndpointHash。
+/// 不访问数据库、不读写文件、不读 env、不输出连接串原文、不输出密码。
+/// 输入为空或空白时抛 ArgumentException,message 不携带 endpoint 原文。
+/// </summary>
+internal static class S8DataSourceEndpointNormalizer
+{
+    public const string KeyAllowPublicKeyRetrieval = "allowpublickeyretrieval";
+    public const string KeyPooling = "pooling";
+    public const string KeyMinimumPoolSize = "minimum pool size";
+    public const string KeyMaximumPoolSize = "maximum pool size";
+    public const string KeyConnectionTimeout = "connection timeout";
+    public const string KeyConnectionIdleTimeout = "connection idle timeout";
+    public const string KeyConnectionLifeTime = "connection lifetime";
+
+    private static readonly (string Key, string Value)[] _normalizeDefaults =
+    {
+        (KeyAllowPublicKeyRetrieval, "True"),
+        (KeyPooling, "True"),
+        (KeyMinimumPoolSize, "0"),
+        (KeyMaximumPoolSize, "20"),
+        (KeyConnectionTimeout, "10"),
+        (KeyConnectionIdleTimeout, "180"),
+        (KeyConnectionLifeTime, "300"),
+    };
+
+    public static S8DataSourceEndpointNormalizeResult Normalize(string rawEndpoint)
+    {
+        if (string.IsNullOrWhiteSpace(rawEndpoint))
+            throw new ArgumentException("S8 data_source endpoint 不能为空或空白", nameof(rawEndpoint));
+
+        var parsed = new DbConnectionStringBuilder { ConnectionString = rawEndpoint };
+
+        // SortedDictionary(Ordinal) + 全部 key 小写化 → 相同语义 endpoint 落到相同 canonical 顺序,hash 稳定。
+        var canonical = new SortedDictionary<string, string>(StringComparer.Ordinal);
+        foreach (string key in parsed.Keys)
+        {
+            var canonicalKey = key.ToLowerInvariant();
+            canonical[canonicalKey] = parsed[key]?.ToString() ?? string.Empty;
+        }
+
+        // 补齐缺失默认值;已有 key(如自定义 Maximum Pool Size)保留原值,不覆盖。
+        foreach (var (defaultKey, defaultValue) in _normalizeDefaults)
+        {
+            if (!canonical.ContainsKey(defaultKey))
+                canonical[defaultKey] = defaultValue;
+        }
+
+        // 用 DbConnectionStringBuilder 按 sorted key 顺序重组;其 ToString 自动处理 value quoting。
+        var normalizedBuilder = new DbConnectionStringBuilder();
+        foreach (var kv in canonical)
+            normalizedBuilder[kv.Key] = kv.Value;
+        var normalized = normalizedBuilder.ConnectionString;
+
+        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(normalized));
+        var hashHex = Convert.ToHexString(bytes).ToLowerInvariant();
+
+        return new S8DataSourceEndpointNormalizeResult(normalized, hashHex);
+    }
+}
+
+/// <summary>
+/// S8 data_source endpoint 规范化结果。NormalizedConnectionString 用于建 SqlSugarScope;
+/// EndpointHash 是 SHA256(normalized) 的 64 字符小写 hex,调用方可取前 N 位派生 ConfigId。
+/// </summary>
+public readonly record struct S8DataSourceEndpointNormalizeResult(string NormalizedConnectionString, string EndpointHash);

+ 4 - 17
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/Rules/S8OutOfRangeRuleEvaluator.cs

@@ -26,13 +26,16 @@ public class S8OutOfRangeRuleEvaluator : IS8RuleEvaluator, ITransient
     private const string DefaultExceptionTypeCode = "EQUIP_FAULT";
 
     private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
+    private readonly S8SqlSugarScopeFactory _scopeFactory;
     private readonly ILogger<S8OutOfRangeRuleEvaluator> _logger;
 
     public S8OutOfRangeRuleEvaluator(
         SqlSugarRepository<AdoS8DataSource> dataSourceRep,
+        S8SqlSugarScopeFactory scopeFactory,
         ILogger<S8OutOfRangeRuleEvaluator> logger)
     {
         _dataSourceRep = dataSourceRep;
+        _scopeFactory = scopeFactory;
         _logger = logger;
     }
 
@@ -78,7 +81,7 @@ public class S8OutOfRangeRuleEvaluator : IS8RuleEvaluator, ITransient
         DataTable table;
         try
         {
-            using var db = CreateSqlScope(dataSource.Endpoint!, timeoutSeconds);
+            using var db = _scopeFactory.CreateScope(dataSource.Endpoint!, _dataSourceRep.Context.CurrentConnectionConfig.DbType, timeoutSeconds);
             table = await db.Ado.GetDataTableAsync(rule.Expression!);
         }
         catch (Exception ex)
@@ -167,22 +170,6 @@ public class S8OutOfRangeRuleEvaluator : IS8RuleEvaluator, ITransient
         return hits;
     }
 
-    private SqlSugarScope CreateSqlScope(string connectionString, int commandTimeoutSeconds)
-    {
-        var dbType = _dataSourceRep.Context.CurrentConnectionConfig.DbType;
-        var scope = new SqlSugarScope(new ConnectionConfig
-        {
-            ConfigId = $"s8-oor-eval-{Guid.NewGuid():N}",
-            DbType = dbType,
-            ConnectionString = connectionString,
-            InitKeyType = InitKeyType.Attribute,
-            IsAutoCloseConnection = true
-        });
-        // 该独立 scope 不继承 SqlSugarSetup.SetDbAop 的全局 30s,必须显式设置。
-        S8EvaluatorGuard.ApplyCommandTimeout(scope, commandTimeoutSeconds);
-        return scope;
-    }
-
     /// <summary>R3 OUT_OF_RANGE dedup_key:与 TIMEOUT/SHORTAGE 同形 T{t}:F{f}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}。</summary>
     internal static string BuildDedupKey(long tenantId, long factoryId, string ruleCode, string sourceObjectType, string sourceObjectId) =>
         $"T{tenantId}:F{factoryId}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}";

+ 4 - 17
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/Rules/S8ShortageRuleEvaluator.cs

@@ -28,13 +28,16 @@ public class S8ShortageRuleEvaluator : IS8RuleEvaluator, ITransient
     private const string SqlDataSourceType = "SQL";
 
     private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
+    private readonly S8SqlSugarScopeFactory _scopeFactory;
     private readonly ILogger<S8ShortageRuleEvaluator> _logger;
 
     public S8ShortageRuleEvaluator(
         SqlSugarRepository<AdoS8DataSource> dataSourceRep,
+        S8SqlSugarScopeFactory scopeFactory,
         ILogger<S8ShortageRuleEvaluator> logger)
     {
         _dataSourceRep = dataSourceRep;
+        _scopeFactory = scopeFactory;
         _logger = logger;
     }
 
@@ -78,7 +81,7 @@ public class S8ShortageRuleEvaluator : IS8RuleEvaluator, ITransient
         DataTable table;
         try
         {
-            using var db = CreateSqlScope(dataSource.Endpoint!, timeoutSeconds);
+            using var db = _scopeFactory.CreateScope(dataSource.Endpoint!, _dataSourceRep.Context.CurrentConnectionConfig.DbType, timeoutSeconds);
             table = await db.Ado.GetDataTableAsync(rule.Expression!);
         }
         catch (Exception ex)
@@ -144,22 +147,6 @@ public class S8ShortageRuleEvaluator : IS8RuleEvaluator, ITransient
         return hits;
     }
 
-    private SqlSugarScope CreateSqlScope(string connectionString, int commandTimeoutSeconds)
-    {
-        var dbType = _dataSourceRep.Context.CurrentConnectionConfig.DbType;
-        var scope = new SqlSugarScope(new ConnectionConfig
-        {
-            ConfigId = $"s8-shortage-eval-{Guid.NewGuid():N}",
-            DbType = dbType,
-            ConnectionString = connectionString,
-            InitKeyType = InitKeyType.Attribute,
-            IsAutoCloseConnection = true
-        });
-        // 该独立 scope 不继承 SqlSugarSetup.SetDbAop 的全局 30s,必须显式设置。
-        S8EvaluatorGuard.ApplyCommandTimeout(scope, commandTimeoutSeconds);
-        return scope;
-    }
-
     /// <summary>构造 R3 dedup_key 稳定字符串:T{tenant}:F{factory}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}。internal 暴露供测试。</summary>
     internal static string BuildDedupKey(long tenantId, long factoryId, string ruleCode, string sourceObjectType, string sourceObjectId) =>
         $"T{tenantId}:F{factoryId}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}";

+ 39 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/Rules/S8SqlSugarScopeFactory.cs

@@ -0,0 +1,39 @@
+using SqlSugar;
+
+namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
+
+/// <summary>
+/// S8-DYNAMIC-SQLSUGAR-SCOPE-FACTORY-1:S8 SQL evaluator 独立 SqlSugarScope 构造工厂。
+/// 职责:
+///   1) 调用 S8DataSourceEndpointNormalizer 规范化 raw endpoint 并派生 EndpointHash;
+///   2) 以 ConfigId="s8-eval-{endpointHash 前 16 位}" 构造 ConnectionConfig:
+///        同 endpoint → 同 ConfigId,让 SqlSugar 内部按 ConfigId dedupe,
+///        并让底层 ADO.NET 连接池按 normalized connection string 与主库共享;
+///   3) 注入 S8EvaluatorGuard.ApplyCommandTimeout 显式 CommandTimeout:
+///        独立 scope 不继承 SqlSugarSetup.SetDbAop 的全局 30s;
+///   4) 返回 SqlSugarScope;不缓存、不持有长生命周期连接、不实现 IDisposable,
+///        调用方负责 using var scope 自动 Dispose。
+/// 不读 endpoint 原文输出日志、不在异常 message 中携带 connection string 或密码。
+/// </summary>
+public sealed class S8SqlSugarScopeFactory : ITransient
+{
+    private const int ConfigIdHashLength = 16;
+    private const string ConfigIdPrefix = "s8-eval-";
+
+    public SqlSugarScope CreateScope(string rawEndpoint, DbType dbType, int commandTimeoutSeconds)
+    {
+        var normalize = S8DataSourceEndpointNormalizer.Normalize(rawEndpoint);
+        var configId = ConfigIdPrefix + normalize.EndpointHash.Substring(0, ConfigIdHashLength);
+
+        var scope = new SqlSugarScope(new ConnectionConfig
+        {
+            ConfigId = configId,
+            DbType = dbType,
+            ConnectionString = normalize.NormalizedConnectionString,
+            InitKeyType = InitKeyType.Attribute,
+            IsAutoCloseConnection = true,
+        });
+        S8EvaluatorGuard.ApplyCommandTimeout(scope, commandTimeoutSeconds);
+        return scope;
+    }
+}

+ 4 - 17
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/Rules/S8TimeoutRuleEvaluator.cs

@@ -23,13 +23,16 @@ public class S8TimeoutRuleEvaluator : IS8RuleEvaluator, ITransient
     private const string SqlDataSourceType = "SQL";
 
     private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
+    private readonly S8SqlSugarScopeFactory _scopeFactory;
     private readonly ILogger<S8TimeoutRuleEvaluator> _logger;
 
     public S8TimeoutRuleEvaluator(
         SqlSugarRepository<AdoS8DataSource> dataSourceRep,
+        S8SqlSugarScopeFactory scopeFactory,
         ILogger<S8TimeoutRuleEvaluator> logger)
     {
         _dataSourceRep = dataSourceRep;
+        _scopeFactory = scopeFactory;
         _logger = logger;
     }
 
@@ -74,7 +77,7 @@ public class S8TimeoutRuleEvaluator : IS8RuleEvaluator, ITransient
         DataTable table;
         try
         {
-            using var db = CreateSqlScope(dataSource.Endpoint!, timeoutSeconds);
+            using var db = _scopeFactory.CreateScope(dataSource.Endpoint!, _dataSourceRep.Context.CurrentConnectionConfig.DbType, timeoutSeconds);
             table = await db.Ado.GetDataTableAsync(rule.Expression!);
         }
         catch (Exception ex)
@@ -136,22 +139,6 @@ public class S8TimeoutRuleEvaluator : IS8RuleEvaluator, ITransient
         return hits;
     }
 
-    private SqlSugarScope CreateSqlScope(string connectionString, int commandTimeoutSeconds)
-    {
-        var dbType = _dataSourceRep.Context.CurrentConnectionConfig.DbType;
-        var scope = new SqlSugarScope(new ConnectionConfig
-        {
-            ConfigId = $"s8-timeout-eval-{Guid.NewGuid():N}",
-            DbType = dbType,
-            ConnectionString = connectionString,
-            InitKeyType = InitKeyType.Attribute,
-            IsAutoCloseConnection = true
-        });
-        // 该独立 scope 不继承 SqlSugarSetup.SetDbAop 的全局 30s,必须显式设置。
-        S8EvaluatorGuard.ApplyCommandTimeout(scope, commandTimeoutSeconds);
-        return scope;
-    }
-
     /// <summary>构造 R2 dedup_key 稳定字符串:T{tenant}:F{factory}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}。internal 暴露供测试。</summary>
     internal static string BuildDedupKey(long tenantId, long factoryId, string ruleCode, string sourceObjectType, string sourceObjectId) =>
         $"T{tenantId}:F{factoryId}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}";