TenantBusinessDatabaseAccessor.cs 3.7 KB
using FoodLabeling.Th.Application.Contracts.Options;
using SqlSugar;
using Volo.Abp;
using Yi.Framework.TenantManagement.Domain;

namespace FoodLabeling.Th.Application.MultiTenancy;

/// <summary>
/// 跨租户操作业务库:解析/回填连接串并用独立 SqlSugarClient 直连,避免 CurrentTenant.Change 解析空连接串。
/// </summary>
public static class TenantBusinessDatabaseAccessor
{
    private const string InitializeTenantDatabaseHintTemplate =
        "请先调用 POST /api/app/th-tenant-provisioning/initialize-tenant-database/{0} 初始化业务库";

    /// <summary>
    /// 解析租户业务库连接串;为空时按配置回填并写回主库 YiTenant。
    /// </summary>
    public static async Task<(string ConnectionString, DbType DbType)> ResolveConnectionAsync(
        TenantAggregateRoot tenant,
        FoodLabelingThTenantDatabaseOptions dbOptions,
        Func<TenantAggregateRoot, Task> persistTenantAsync)
    {
        var connectionString = tenant.TenantConnectionString?.Trim();
        if (!string.IsNullOrWhiteSpace(connectionString))
        {
            return (connectionString, ResolveDbType(tenant.DbType));
        }

        connectionString = TenantDatabaseConnectionStringBuilder.BuildMySqlConnectionString(
            dbOptions,
            tenant.Name);

        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw BuildMissingConnectionStringException(tenant.Name);
        }

        var dbType = DbType.MySql;
        tenant.SetConnectionString(dbType, connectionString);
        await persistTenantAsync(tenant);

        return (connectionString, dbType);
    }

    /// <summary>
    /// 创建直连租户业务库的 SqlSugar 客户端(调用方 using 释放)。
    /// </summary>
    public static SqlSugarClient CreateClient(string connectionString, DbType dbType)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new UserFriendlyException("租户业务库连接串不能为空");
        }

        return new SqlSugarClient(new ConnectionConfig
        {
            ConfigId = $"th-tenant-direct-{Guid.NewGuid():N}",
            DbType = dbType,
            ConnectionString = connectionString,
            IsAutoCloseConnection = true
        });
    }

    /// <summary>
    /// 探测业务库是否可连;库不存在时给出初始化引导。
    /// </summary>
    public static void EnsureDatabaseReachable(ISqlSugarClient db, Guid tenantId)
    {
        try
        {
            db.Ado.ExecuteCommand("SELECT 1");
        }
        catch (Exception ex) when (IsDatabaseNotExistError(ex))
        {
            throw new UserFriendlyException(
                string.Format(InitializeTenantDatabaseHintTemplate, tenantId));
        }
    }

    public static UserFriendlyException BuildMissingConnectionStringException(string tenantName)
    {
        return new UserFriendlyException(
            $"租户「{tenantName}」未配置业务库连接串,请先在租户管理完善连接串或调用开通/初始化接口");
    }

    private static DbType ResolveDbType(DbType tenantDbType)
    {
        return tenantDbType == default ? DbType.MySql : tenantDbType;
    }

    private static bool IsDatabaseNotExistError(Exception ex)
    {
        var message = ex.Message;
        var current = ex;
        while (current.InnerException != null)
        {
            current = current.InnerException;
            message += " " + current.Message;
        }

        return message.Contains("Unknown database", StringComparison.OrdinalIgnoreCase)
               || message.Contains("doesn't exist", StringComparison.OrdinalIgnoreCase)
               || message.Contains("1049", StringComparison.Ordinal);
    }
}