ThTenantProvisioningAppService.cs 5.95 KB
using FoodLabeling.Th.Application.Contracts.Dtos.MultiTenancy;
using FoodLabeling.Th.Application.Contracts.IServices;
using FoodLabeling.Th.Application.Contracts.Options;
using FoodLabeling.Th.Application.MultiTenancy;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Timeouts;
using Microsoft.Extensions.Options;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.TenantManagement.Application.Contracts;
using Yi.Framework.TenantManagement.Application.Contracts.Dtos;

namespace FoodLabeling.Th.Application.Services;

/// <summary>
/// 泰额版租户独立库开通(平台主库 yitenant + 租户业务库)
/// </summary>
/// <remarks>
/// 脏数据修复(业务库 antis-foodlabeling-us 误写入的 YiTenant 迁回主库 antis-foodlabeling-host,名称冲突则跳过):
/// <code>
/// INSERT INTO `antis-foodlabeling-host`.YiTenant
///   (Id, Name, TenantConnectionString, DbType, EntityVersion,
///    ConcurrencyStamp, CreationTime, CreatorId,
///    LastModificationTime, LastModifierId, IsDeleted, DeleterId, DeletionTime)
/// SELECT u.Id, u.Name, u.TenantConnectionString, u.DbType, u.EntityVersion,
///    u.ConcurrencyStamp, u.CreationTime, u.CreatorId,
///    u.LastModificationTime, u.LastModifierId, u.IsDeleted, u.DeleterId, u.DeletionTime
/// FROM `antis-foodlabeling-us`.YiTenant u
/// WHERE u.Name IN ('mike', '中国麦当劳公司')
///   AND NOT EXISTS (
///     SELECT 1 FROM `antis-foodlabeling-host`.YiTenant h WHERE h.Name = u.Name OR h.Id = u.Id
///   );
/// -- 确认主库可查后再按需清理业务库副本(谨慎执行):
/// -- DELETE FROM `antis-foodlabeling-us`.YiTenant WHERE Name IN ('mike', '中国麦当劳公司');
/// </code>
/// </remarks>
[Authorize]
public class ThTenantProvisioningAppService : ApplicationService, IThTenantProvisioningAppService
{
    /// <summary>同步初始化路径请求超时策略名,与 Web 模块 <c>ThTenantDatabaseInit</c> 一致</summary>
    public const string TenantDatabaseInitRequestTimeoutPolicy = "ThTenantDatabaseInit";

    private readonly ITenantService _tenantService;
    private readonly IThTenantDatabaseBackgroundInitializer _backgroundInitializer;
    private readonly FoodLabelingThTenantDatabaseOptions _dbOptions;
    private readonly DbConnOptions _dbConnOptions;

    public ThTenantProvisioningAppService(
        ITenantService tenantService,
        IThTenantDatabaseBackgroundInitializer backgroundInitializer,
        IOptions<FoodLabelingThTenantDatabaseOptions> dbOptions,
        IOptions<DbConnOptions> dbConnOptions)
    {
        _tenantService = tenantService;
        _backgroundInitializer = backgroundInitializer;
        _dbOptions = dbOptions.Value;
        _dbConnOptions = dbConnOptions.Value;
    }

    /// <inheritdoc />
    public virtual async Task<ThProvisionTenantOutputDto> ProvisionAsync(ThProvisionTenantInputVo input)
    {
        if (string.IsNullOrWhiteSpace(input.Name))
        {
            throw new UserFriendlyException("租户名称不能为空");
        }

        var connectionString = string.IsNullOrWhiteSpace(input.TenantConnectionString)
            ? TenantDatabaseConnectionStringBuilder.BuildMySqlConnectionString(
                _dbOptions,
                input.Name,
                input.DatabaseKey)
            : input.TenantConnectionString.Trim();

        var databaseName = ExtractDatabaseName(connectionString)
                           ?? TenantDatabaseConnectionStringBuilder.BuildDatabaseName(
                               _dbOptions,
                               input.Name,
                               input.DatabaseKey);

        // 平台主库写入 yitenant(CurrentTenant 为空时走 DbConnOptions 主库)
        var dbType = Enum.IsDefined(typeof(DbType), input.DbType)
            ? (DbType)input.DbType
            : DbType.MySql;

        // YiTenant 必须写入主库;Create 与 Init 分事务提交,Init 经 TenantStore 读主库需可见记录
        TenantGetOutputDto created;
        using (CurrentTenant.Change(null))
        using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: true))
        {
            created = await _tenantService.CreateAsync(new TenantCreateInput
            {
                Name = input.Name.Trim(),
                TenantConnectionString = connectionString,
                DbType = dbType
            });
            await uow.CompleteAsync();
        }

        var initializing = false;
        if (input.InitializeDatabase)
        {
            // 同步建库 + GRANT,避免仅入队后台 Init 时库不存在导致静默失败
            TenantDatabaseBootstrapper.EnsureDatabaseCreated(
                _dbConnOptions,
                dbType,
                connectionString,
                databaseName);

            _backgroundInitializer.Enqueue(created.Id);
            initializing = true;
        }

        return new ThProvisionTenantOutputDto
        {
            TenantId = created.Id,
            Name = created.Name,
            DatabaseName = databaseName,
            TenantConnectionString = connectionString,
            DatabaseInitialized = false,
            DatabaseInitializing = initializing
        };
    }

    /// <inheritdoc />
    [RequestTimeout(TenantDatabaseInitRequestTimeoutPolicy)]
    public virtual Task InitializeTenantDatabaseAsync(Guid tenantId)
    {
        return _tenantService.InitAsync(tenantId);
    }

    private static string? ExtractDatabaseName(string connectionString)
    {
        const string key = "database=";
        var idx = connectionString.IndexOf(key, StringComparison.OrdinalIgnoreCase);
        if (idx < 0)
        {
            return null;
        }

        var start = idx + key.Length;
        var end = connectionString.IndexOf(';', start);
        return end < 0
            ? connectionString[start..].Trim()
            : connectionString[start..end].Trim();
    }
}