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; /// /// 泰额版租户独立库开通(平台主库 yitenant + 租户业务库) /// /// /// 脏数据修复(业务库 antis-foodlabeling-us 误写入的 YiTenant 迁回主库 antis-foodlabeling-host,名称冲突则跳过): /// /// 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', '中国麦当劳公司'); /// /// [Authorize] public class ThTenantProvisioningAppService : ApplicationService, IThTenantProvisioningAppService { /// 同步初始化路径请求超时策略名,与 Web 模块 ThTenantDatabaseInit 一致 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 dbOptions, IOptions dbConnOptions) { _tenantService = tenantService; _backgroundInitializer = backgroundInitializer; _dbOptions = dbOptions.Value; _dbConnOptions = dbConnOptions.Value; } /// public virtual async Task 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 }; } /// [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(); } }