ThTenantProvisioningAppService.cs
5.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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();
}
}