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.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; } } }