diff --git a/src/Api/TakeoutSaaS.TenantApi/Program.cs b/src/Api/TakeoutSaaS.TenantApi/Program.cs
index b20bcb6..9802d8d 100644
--- a/src/Api/TakeoutSaaS.TenantApi/Program.cs
+++ b/src/Api/TakeoutSaaS.TenantApi/Program.cs
@@ -85,8 +85,7 @@ if (isDevelopment)
});
}
-// 5. 注册多租户解析、鉴权授权与权限策略
-builder.Services.AddTenantResolution(builder.Configuration);
+// 5. 注册鉴权授权与权限策略
builder.Services.AddJwtAuthentication(builder.Configuration);
builder.Services.AddAuthorization();
builder.Services.AddPermissionAuthorization();
@@ -98,6 +97,9 @@ builder.Services.AddIdentityApplication(enableMiniSupport: false);
builder.Services.AddAppInfrastructure(builder.Configuration);
builder.Services.AddIdentityInfrastructure(builder.Configuration, enableMiniFeatures: false, enableAdminSeed: false);
+// 7. 注册多租户解析(依赖 ITenantRepository,需在 Infrastructure 之后)
+builder.Services.AddTenantResolution(builder.Configuration);
+
// 6. (空行后) 注册字典模块(系统参数、字典项、缓存等)
builder.Services.AddDictionaryApplication();
builder.Services.AddDictionaryInfrastructure(builder.Configuration);
diff --git a/src/Api/TakeoutSaaS.TenantApi/appsettings.Development.json b/src/Api/TakeoutSaaS.TenantApi/appsettings.Development.json
index 28e2b17..68b96da 100644
--- a/src/Api/TakeoutSaaS.TenantApi/appsettings.Development.json
+++ b/src/Api/TakeoutSaaS.TenantApi/appsettings.Development.json
@@ -56,7 +56,9 @@
"/health",
"/healthz"
],
- "RootDomain": ""
+ "RootDomain": "laosankeji.com",
+ "CodeTenantMap": {},
+ "ThrowIfUnresolved": false
},
"Cors": {
"Tenant": []
diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs
index 3046546..e03ee2f 100644
--- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs
@@ -84,6 +84,14 @@ public interface ITenantRepository
/// 存在返回 true,否则 false。
Task ExistsByCodeAsync(string code, CancellationToken cancellationToken = default);
+ ///
+ /// 依据租户编码查询租户 ID。
+ ///
+ /// 租户编码。
+ /// 取消标记。
+ /// 租户 ID,未找到返回 null。
+ Task FindIdByCodeAsync(string code, CancellationToken cancellationToken = default);
+
///
/// 判断租户名称是否存在(支持排除指定租户)。
///
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs
index 2b091a0..cd56dff 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs
@@ -162,6 +162,20 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsD
return context.Tenants.AnyAsync(x => x.Code == normalized, cancellationToken);
}
+ ///
+ public Task FindIdByCodeAsync(string code, CancellationToken cancellationToken = default)
+ {
+ // 1. 标准化编码
+ var normalized = code.Trim();
+
+ // 2. 查询租户 ID(仅查询未删除且状态正常的租户)
+ return context.Tenants
+ .AsNoTracking()
+ .Where(x => x.Code == normalized && x.DeletedAt == null)
+ .Select(x => (long?)x.Id)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
///
public Task ExistsByNameAsync(string name, long? excludeTenantId = null, CancellationToken cancellationToken = default)
{
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs
index 7f6ad99..ed63222 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs
@@ -2,65 +2,37 @@ using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Identity.Entities;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Infrastructure.Identity.Persistence;
-using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Infrastructure.Identity.Repositories;
///
/// 菜单仓储 EF 实现。
///
-public sealed class EfMenuRepository(IdentityDbContext dbContext, ITenantContextAccessor tenantContextAccessor) : IMenuRepository
+public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuRepository
{
///
public async Task> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default)
{
- // 1. 优先返回租户自定义菜单
+ // 1. 仅返回该租户的菜单,无回退逻辑
var tenantMenus = await dbContext.MenuDefinitions
.AsNoTracking()
- .Where(x => x.TenantId == tenantId)
+ .Where(x => x.TenantId == tenantId && x.DeletedAt == null)
.OrderBy(x => x.ParentId)
.ThenBy(x => x.SortOrder)
.ToListAsync(cancellationToken);
- if (tenantMenus.Count > 0)
- {
- return tenantMenus;
- }
- // 2. (空行后) 回退系统默认菜单(TenantId=0)
- using (tenantContextAccessor.EnterTenantScope(0, "menu"))
- {
- var systemMenus = await dbContext.MenuDefinitions
- .AsNoTracking()
- .Where(x => x.TenantId == 0 && x.DeletedAt == null)
- .OrderBy(x => x.ParentId)
- .ThenBy(x => x.SortOrder)
- .ToListAsync(cancellationToken);
-
- return systemMenus;
- }
+ return tenantMenus;
}
///
public async Task FindByIdAsync(long id, long tenantId, CancellationToken cancellationToken = default)
{
- // 1. 优先查租户菜单
- var tenantMenu = await dbContext.MenuDefinitions
+ // 1. 仅查询该租户的菜单,无回退逻辑
+ return await dbContext.MenuDefinitions
.AsNoTracking()
.FirstOrDefaultAsync(
- x => x.Id == id && x.TenantId == tenantId,
+ x => x.Id == id && x.TenantId == tenantId && x.DeletedAt == null,
cancellationToken);
- if (tenantMenu != null)
- {
- return tenantMenu;
- }
-
- // 2. (空行后) 回退查系统默认菜单(TenantId=0)
- using (tenantContextAccessor.EnterTenantScope(0, "menu"))
- {
- return await dbContext.MenuDefinitions
- .AsNoTracking()
- .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == 0 && x.DeletedAt == null, cancellationToken);
- }
}
///
diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/DatabaseTenantCodeResolver.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/DatabaseTenantCodeResolver.cs
new file mode 100644
index 0000000..943f3a7
--- /dev/null
+++ b/src/Modules/TakeoutSaaS.Module.Tenancy/DatabaseTenantCodeResolver.cs
@@ -0,0 +1,22 @@
+using TakeoutSaaS.Domain.Tenants.Repositories;
+
+namespace TakeoutSaaS.Module.Tenancy;
+
+///
+/// 基于数据库的租户编码解析器实现。
+///
+public sealed class DatabaseTenantCodeResolver(ITenantRepository tenantRepository) : ITenantCodeResolver
+{
+ ///
+ public Task ResolveAsync(string code, CancellationToken cancellationToken = default)
+ {
+ // 1. 参数校验
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return Task.FromResult(null);
+ }
+
+ // 2. 从数据库查询租户 ID
+ return tenantRepository.FindIdByCodeAsync(code, cancellationToken);
+ }
+}
diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs
index 72afde3..eeaa0a7 100644
--- a/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs
+++ b/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs
@@ -18,6 +18,7 @@ public static class TenantServiceCollectionExtensions
{
services.TryAddSingleton();
services.TryAddScoped();
+ services.TryAddScoped();
services.AddOptions()
.Bind(configuration.GetSection("Tenancy"))
diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/ITenantCodeResolver.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/ITenantCodeResolver.cs
new file mode 100644
index 0000000..064137c
--- /dev/null
+++ b/src/Modules/TakeoutSaaS.Module.Tenancy/ITenantCodeResolver.cs
@@ -0,0 +1,15 @@
+namespace TakeoutSaaS.Module.Tenancy;
+
+///
+/// 租户编码解析器:根据租户编码查询租户 ID。
+///
+public interface ITenantCodeResolver
+{
+ ///
+ /// 根据租户编码查询租户 ID。
+ ///
+ /// 租户编码。
+ /// 取消令牌。
+ /// 租户 ID,未找到返回 null。
+ Task ResolveAsync(string code, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj
index fdb3727..facba47 100644
--- a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj
+++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs
index be230e0..60fa5ca 100644
--- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs
+++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs
@@ -22,7 +22,7 @@ public sealed class TenantResolutionMiddleware(
///
/// 解析租户并将上下文注入请求。
///
- public async Task InvokeAsync(HttpContext context)
+ public async Task InvokeAsync(HttpContext context, ITenantCodeResolver tenantCodeResolver)
{
var options = optionsMonitor.CurrentValue ?? new TenantResolutionOptions();
if (ShouldSkip(context.Request.Path, options))
@@ -31,7 +31,7 @@ public sealed class TenantResolutionMiddleware(
return;
}
- var tenantContext = ResolveTenant(context, options);
+ var tenantContext = await ResolveTenantAsync(context, options, tenantCodeResolver);
tenantContextAccessor.Current = tenantContext;
context.Items[TenantConstants.HttpContextItemKey] = tenantContext;
@@ -86,7 +86,10 @@ public sealed class TenantResolutionMiddleware(
});
}
- private static TenantContext ResolveTenant(HttpContext context, TenantResolutionOptions options)
+ private static async Task ResolveTenantAsync(
+ HttpContext context,
+ TenantResolutionOptions options,
+ ITenantCodeResolver tenantCodeResolver)
{
var request = context.Request;
var isAuthenticated = context.User?.Identity?.IsAuthenticated == true;
@@ -117,9 +120,10 @@ public sealed class TenantResolutionMiddleware(
request.Headers.TryGetValue(options.TenantCodeHeaderName, out var codeHeader))
{
var code = codeHeader.FirstOrDefault();
- if (TryResolveByCode(code, options, out var tenantFromCode))
+ var tenantFromCode = await ResolveByCodeAsync(code, options, tenantCodeResolver, context.RequestAborted);
+ if (tenantFromCode.HasValue)
{
- return new TenantContext(tenantFromCode, code, $"header:{options.TenantCodeHeaderName}");
+ return new TenantContext(tenantFromCode.Value, code, $"header:{options.TenantCodeHeaderName}");
}
}
@@ -127,30 +131,43 @@ public sealed class TenantResolutionMiddleware(
var host = request.Host.Host;
if (!string.IsNullOrWhiteSpace(host))
{
+ // 4.1 精确域名映射
if (options.DomainTenantMap.TryGetValue(host, out var tenantFromHost) && tenantFromHost > 0)
{
return new TenantContext(tenantFromHost, null, $"host:{host}");
}
+ // 4.2 子域名解析
var codeFromHost = ResolveCodeFromHost(host, options.RootDomain);
- if (TryResolveByCode(codeFromHost, options, out var tenantFromSubdomain))
+ var tenantFromSubdomain = await ResolveByCodeAsync(codeFromHost, options, tenantCodeResolver, context.RequestAborted);
+ if (tenantFromSubdomain.HasValue)
{
- return new TenantContext(tenantFromSubdomain, codeFromHost, $"host:{host}");
+ return new TenantContext(tenantFromSubdomain.Value, codeFromHost, $"host:{host}");
}
}
return TenantContext.Empty;
}
- private static bool TryResolveByCode(string? code, TenantResolutionOptions options, out long tenantId)
+ private static async Task ResolveByCodeAsync(
+ string? code,
+ TenantResolutionOptions options,
+ ITenantCodeResolver tenantCodeResolver,
+ CancellationToken cancellationToken)
{
- tenantId = 0;
if (string.IsNullOrWhiteSpace(code))
{
- return false;
+ return null;
}
- return options.CodeTenantMap.TryGetValue(code, out tenantId) && tenantId > 0;
+ // 1. 优先从静态配置查找(兼容旧配置)
+ if (options.CodeTenantMap.TryGetValue(code, out var tenantId) && tenantId > 0)
+ {
+ return tenantId;
+ }
+
+ // 2. 从数据库动态查询
+ return await tenantCodeResolver.ResolveAsync(code, cancellationToken);
}
private static string? ResolveCodeFromHost(string host, string? rootDomain)