ThTenantHostDataAccessor.cs
9.23 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
using FoodLabeling.Th.Domain.Entities;
using SqlSugar;
using Volo.Abp;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.TenantManagement.Domain;
namespace FoodLabeling.Th.Application.MultiTenancy;
/// <summary>
/// 主库(DbConnOptions.Url)直连读取 YiTenant、fl_th_tenant_admin_credential,
/// 避免后台 Init 后 UoW 缓存租户库连接导致凭据查不到,以及 FindAsync 与 TenantId 列映射不一致。
/// </summary>
internal static class ThTenantHostDataAccessor
{
internal static async Task<TenantAggregateRoot?> LoadTenantAsync(DbConnOptions dbConnOptions, Guid tenantId)
{
if (string.IsNullOrWhiteSpace(dbConnOptions.Url))
{
throw new UserFriendlyException("未配置主库连接字符串 DbConnOptions.Url");
}
var dbType = dbConnOptions.DbType ?? DbType.MySql;
using var client = CreateHostClient(dbConnOptions.Url, dbType);
var tenant = await client.Queryable<TenantAggregateRoot>().InSingleAsync(tenantId);
return tenant is { IsDeleted: true } ? null : tenant;
}
internal static async Task<ThTenantAdminCredentialEntity?> LoadCredentialAsync(
DbConnOptions dbConnOptions,
Guid tenantId)
{
if (string.IsNullOrWhiteSpace(dbConnOptions.Url))
{
throw new UserFriendlyException("未配置主库连接字符串 DbConnOptions.Url");
}
var dbType = dbConnOptions.DbType ?? DbType.MySql;
using var client = CreateHostClient(dbConnOptions.Url, dbType);
var credentials = await client.Queryable<ThTenantAdminCredentialEntity>()
.Where(c => c.Id == tenantId)
.Take(1)
.ToListAsync();
return credentials.FirstOrDefault();
}
/// <summary>
/// 按开通邮箱反查未删除租户 Id(邮箱全局唯一时返回唯一租户;同邮箱多条取最近修改)。
/// </summary>
internal static async Task<Guid?> FindActiveTenantIdByLoginAccountAsync(
DbConnOptions dbConnOptions,
string loginAccount)
{
if (string.IsNullOrWhiteSpace(loginAccount) || string.IsNullOrWhiteSpace(dbConnOptions.Url))
{
return null;
}
var normalized = loginAccount.Trim().ToLowerInvariant();
var dbType = dbConnOptions.DbType ?? DbType.MySql;
using var client = CreateHostClient(dbConnOptions.Url, dbType);
// YiTenant.Id 与 fl_th_tenant_admin_credential 列 collation 不一致,ORM Join 会报错
const string sql = """
SELECT CAST(c.`TenantId` AS CHAR(36)) AS TenantId
FROM `fl_th_tenant_admin_credential` c
INNER JOIN `YiTenant` t
ON CONVERT(CAST(c.`TenantId` AS CHAR(36)) USING utf8mb4) COLLATE utf8mb4_general_ci
= CONVERT(CAST(t.`Id` AS CHAR(36)) USING utf8mb4) COLLATE utf8mb4_general_ci
WHERE t.`IsDeleted` = 0
AND c.`LoginAccount` IS NOT NULL
AND LOWER(CONVERT(c.`LoginAccount` USING utf8mb4) COLLATE utf8mb4_general_ci) = @LoginAccount
ORDER BY c.`LastModificationTime` DESC
LIMIT 1
""";
// SqlSugar SqlQueryAsync<string> 对单列映射不稳定,使用 DTO
var rows = await client.Ado.SqlQueryAsync<TenantIdSqlRow>(sql, new { LoginAccount = normalized });
var raw = rows.FirstOrDefault()?.TenantId;
if (string.IsNullOrWhiteSpace(raw) || !Guid.TryParse(raw.Trim(), out var tenantId))
{
return null;
}
return tenantId;
}
/// <summary>
/// 按租户业务库 user 表 Email/UserName 反查公司租户(Team Member 等非管理员账号登录)。
/// </summary>
internal static async Task<Guid?> FindActiveTenantIdByBusinessUserLoginAsync(
DbConnOptions dbConnOptions,
string loginAccount)
{
if (string.IsNullOrWhiteSpace(loginAccount) || string.IsNullOrWhiteSpace(dbConnOptions.Url))
{
return null;
}
var normalized = loginAccount.Trim().ToLowerInvariant();
var hostDbType = dbConnOptions.DbType ?? DbType.MySql;
using var hostClient = CreateHostClient(dbConnOptions.Url, hostDbType);
var tenants = await hostClient.Queryable<TenantAggregateRoot>()
.Where(t => !t.IsDeleted)
.Where(t => t.TenantConnectionString != null && t.TenantConnectionString != string.Empty)
.Select(t => new { t.Id, t.TenantConnectionString, t.DbType })
.ToListAsync();
foreach (var tenant in tenants)
{
var connectionString = tenant.TenantConnectionString?.Trim();
if (string.IsNullOrWhiteSpace(connectionString))
{
continue;
}
try
{
var tenantDbType = tenant.DbType == default ? hostDbType : tenant.DbType;
using var tenantClient = TenantBusinessDatabaseAccessor.CreateClient(connectionString, tenantDbType);
tenantClient.Ado.CommandTimeOut = 8;
const string sql = """
SELECT CAST(`Id` AS CHAR(36)) AS UserId
FROM `user`
WHERE `IsDeleted` = 0
AND `State` = 1
AND (
(`Email` IS NOT NULL AND LOWER(TRIM(`Email`)) = @LoginAccount)
OR LOWER(TRIM(`UserName`)) = @LoginAccount
)
LIMIT 1
""";
var rows = await tenantClient.Ado.SqlQueryAsync<UserIdSqlRow>(sql, new { LoginAccount = normalized });
if (rows.Count > 0 && !string.IsNullOrWhiteSpace(rows[0].UserId))
{
return tenant.Id;
}
}
catch
{
// 单租户库不可达时跳过,继续扫描其它租户
}
}
return null;
}
internal static async Task<List<string>> LoadMenuPermissionKeysAsync(
DbConnOptions dbConnOptions,
Guid tenantId)
{
if (string.IsNullOrWhiteSpace(dbConnOptions.Url))
{
throw new UserFriendlyException("未配置主库连接字符串 DbConnOptions.Url");
}
var dbType = dbConnOptions.DbType ?? DbType.MySql;
using var client = CreateHostClient(dbConnOptions.Url, dbType);
return await client.Queryable<ThTenantMenuPermissionEntity>()
.Where(x => x.TenantId == tenantId)
.OrderBy(x => x.CreationTime)
.Select(x => x.PermissionKey)
.ToListAsync();
}
internal static async Task<List<MenuAggregateRoot>> LoadHostMenusAsync(DbConnOptions dbConnOptions)
{
if (string.IsNullOrWhiteSpace(dbConnOptions.Url))
{
throw new UserFriendlyException("未配置主库连接字符串 DbConnOptions.Url");
}
var dbType = dbConnOptions.DbType ?? DbType.MySql;
using var client = CreateHostClient(dbConnOptions.Url, dbType);
return await client.Queryable<MenuAggregateRoot>()
.Where(x => !x.IsDeleted)
.OrderBy(x => x.OrderNum)
.ToListAsync();
}
internal static async Task UpdateTenantAsync(DbConnOptions dbConnOptions, TenantAggregateRoot tenant)
{
if (string.IsNullOrWhiteSpace(dbConnOptions.Url))
{
throw new UserFriendlyException("未配置主库连接字符串 DbConnOptions.Url");
}
var dbType = dbConnOptions.DbType ?? DbType.MySql;
using var client = CreateHostClient(dbConnOptions.Url, dbType);
await client.Updateable(tenant).ExecuteCommandAsync();
}
private static SqlSugarClient CreateHostClient(string connectionString, DbType dbType)
{
return new SqlSugarClient(new ConnectionConfig
{
ConfigId = $"th-host-read-{Guid.NewGuid():N}",
DbType = dbType,
ConnectionString = connectionString,
IsAutoCloseConnection = true,
MoreSettings = new ConnMoreSettings
{
// 避免反查登录租户时长时间卡在不可达库
IsAutoRemoveDataCache = true
},
AopEvents = new AopEvents
{
OnError = _ => { }
},
// 与 DefaultSqlSugarDbContext 一致:主库 menu 等表无 ExtraProperties 列
ConfigureExternalServices = new ConfigureExternalServices
{
EntityService = (propertyInfo, columnInfo) =>
{
if (propertyInfo.PropertyType == typeof(Volo.Abp.Data.ExtraPropertyDictionary))
{
columnInfo.IsIgnore = true;
}
if (propertyInfo.Name == nameof(Volo.Abp.Domain.Entities.Entity<object>.Id))
{
columnInfo.IsPrimarykey = true;
}
}
}
},
db =>
{
db.Ado.CommandTimeOut = 8;
});
}
private sealed class TenantIdSqlRow
{
public string? TenantId { get; set; }
}
private sealed class UserIdSqlRow
{
public string? UserId { get; set; }
}
}