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;
///
/// 主库(DbConnOptions.Url)直连读取 YiTenant、fl_th_tenant_admin_credential,
/// 避免后台 Init 后 UoW 缓存租户库连接导致凭据查不到,以及 FindAsync 与 TenantId 列映射不一致。
///
internal static class ThTenantHostDataAccessor
{
internal static async Task 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().InSingleAsync(tenantId);
return tenant is { IsDeleted: true } ? null : tenant;
}
internal static async Task 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()
.Where(c => c.Id == tenantId)
.Take(1)
.ToListAsync();
return credentials.FirstOrDefault();
}
///
/// 按开通邮箱反查未删除租户 Id(邮箱全局唯一时返回唯一租户;同邮箱多条取最近修改)。
///
internal static async Task 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 对单列映射不稳定,使用 DTO
var rows = await client.Ado.SqlQueryAsync(sql, new { LoginAccount = normalized });
var raw = rows.FirstOrDefault()?.TenantId;
if (string.IsNullOrWhiteSpace(raw) || !Guid.TryParse(raw.Trim(), out var tenantId))
{
return null;
}
return tenantId;
}
///
/// 按租户业务库 user 表 Email/UserName 反查公司租户(Team Member 等非管理员账号登录)。
///
internal static async Task 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()
.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(sql, new { LoginAccount = normalized });
if (rows.Count > 0 && !string.IsNullOrWhiteSpace(rows[0].UserId))
{
return tenant.Id;
}
}
catch
{
// 单租户库不可达时跳过,继续扫描其它租户
}
}
return null;
}
internal static async Task> 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()
.Where(x => x.TenantId == tenantId)
.OrderBy(x => x.CreationTime)
.Select(x => x.PermissionKey)
.ToListAsync();
}
internal static async Task> 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()
.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