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();
}
}