TenantDatabaseConnectionStringBuilder.cs 3.1 KB
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using FoodLabeling.Th.Application.Contracts.Options;
using SqlSugar;

namespace FoodLabeling.Th.Application.MultiTenancy;

/// <summary>
/// 根据配置生成 MySQL 租户库连接串
/// </summary>
public static class TenantDatabaseConnectionStringBuilder
{
    private const int FallbackHashLength = 8;
    private static readonly Regex SafeNameRegex = new(@"[^a-z0-9_]", RegexOptions.Compiled);

    /// <summary>
    /// 将租户名或显式 DatabaseKey 规范化为 MySQL 库名片段(小写字母、数字、下划线)。
    /// 纯中文等非 ASCII 名称在清洗为空时,使用基于名称的稳定 hash 兜底(t + 8 位十六进制)。
    /// </summary>
    public static string NormalizeTenantDatabaseKey(string tenantName, string? explicitDatabaseKey = null)
    {
        if (!string.IsNullOrWhiteSpace(explicitDatabaseKey))
        {
            return NormalizeSanitizedKey(explicitDatabaseKey.Trim(), "DatabaseKey 无效,无法生成数据库名");
        }

        var key = SanitizeToDatabaseKey(tenantName);
        if (string.IsNullOrEmpty(key))
        {
            key = BuildStableFallbackKey(tenantName);
        }

        return key;
    }

    public static string BuildDatabaseName(
        FoodLabelingThTenantDatabaseOptions options,
        string tenantName,
        string? databaseKey = null)
    {
        var key = NormalizeTenantDatabaseKey(tenantName, databaseKey);
        return options.DatabaseNameTemplate.Replace("{tenant}", key, StringComparison.OrdinalIgnoreCase);
    }

    public static string BuildMySqlConnectionString(
        FoodLabelingThTenantDatabaseOptions options,
        string tenantName,
        string? databaseKey = null)
    {
        var database = BuildDatabaseName(options, tenantName, databaseKey);
        return
            $"server={options.Server};port={options.Port};database={database};uid={options.UserId};pwd={options.Password};CharSet={options.CharSet};";
    }

    public static DbType ResolveDbType(string? configuredDbType)
    {
        return Enum.TryParse<DbType>(configuredDbType, true, out var dbType) ? dbType : DbType.MySql;
    }

    private static string SanitizeToDatabaseKey(string value)
    {
        var key = value.Trim().ToLowerInvariant();
        key = SafeNameRegex.Replace(key, "_");
        return Regex.Replace(key, "_+", "_").Trim('_');
    }

    private static string NormalizeSanitizedKey(string value, string invalidMessage)
    {
        var key = SanitizeToDatabaseKey(value);
        if (string.IsNullOrEmpty(key))
        {
            throw new ArgumentException(invalidMessage);
        }

        return key;
    }

    /// <summary>
    /// 基于租户名称生成稳定、可重复的短 key,避免纯非 ASCII 名称清洗后为空。
    /// </summary>
    private static string BuildStableFallbackKey(string tenantName)
    {
        var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(tenantName.Trim()));
        var hex = Convert.ToHexString(hashBytes).ToLowerInvariant();
        return "t" + hex[..FallbackHashLength];
    }
}