From d7821fa1af9dfb3309c4d5cc48ca2f8a08666ae5 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Thu, 29 Jan 2026 04:53:37 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E5=85=BC=E5=AE=B9=E5=B9=B6=E7=BB=9F=E4=B8=80=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E7=A7=9F=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TakeoutSaaS.Docs | 2 +- .../DictionaryLabelOverridesController.cs | 53 +++------ .../DictionaryOverridesController.cs | 81 +++++-------- .../TenantAnnouncementsController.cs | 110 +++++++----------- .../Controllers/TenantRolesController.cs | 84 +++---------- .../Controllers/TenantsController.cs | 2 +- .../Filters/TenantRouteContextFilter.cs | 66 +++++++++++ src/Api/TakeoutSaaS.AdminApi/Program.cs | 9 ++ 8 files changed, 186 insertions(+), 221 deletions(-) create mode 100644 src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs diff --git a/TakeoutSaaS.Docs b/TakeoutSaaS.Docs index 88ad710..318aded 160000 --- a/TakeoutSaaS.Docs +++ b/TakeoutSaaS.Docs @@ -1 +1 @@ -Subproject commit 88ad71041bcec59715274c9ca568b0ca434408a1 +Subproject commit 318aded4bf7a3b05d8012b5199eaba26bdbb36b0 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryLabelOverridesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryLabelOverridesController.cs index d031058..2f50743 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryLabelOverridesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryLabelOverridesController.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; @@ -20,31 +19,29 @@ namespace TakeoutSaaS.AdminApi.Controllers; [Route("api/admin/v{version:apiVersion}/dictionary/label-overrides")] public sealed class DictionaryLabelOverridesController( DictionaryLabelOverrideService labelOverrideService, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor) : BaseApiController { - private const string TenantIdHeaderName = "X-Tenant-Id"; - #region 租户端 API(租户覆盖系统字典) /// /// 获取当前租户的标签覆盖列表。 /// - [HttpGet("tenant")] + [HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides")] [PermissionAuthorize("dictionary:override:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] public async Task>> ListTenantOverrides( + long tenantId, [FromQuery] OverrideType? overrideType, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader>(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse>.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 查询租户覆盖列表 var result = await labelOverrideService.GetOverridesAsync(tenantId, overrideType, cancellationToken); return ApiResponse>.Ok(result); } @@ -52,20 +49,21 @@ public sealed class DictionaryLabelOverridesController( /// /// 租户覆盖系统字典项的标签。 /// - [HttpPost("tenant")] + [HttpPost("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides")] [PermissionAuthorize("dictionary:override:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> CreateTenantOverride( + long tenantId, [FromBody] UpsertLabelOverrideRequest request, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 执行租户覆盖 var operatorId = currentUserAccessor.UserId; var result = await labelOverrideService.UpsertTenantOverrideAsync(tenantId, request, operatorId, cancellationToken); return ApiResponse.Ok(result); @@ -74,19 +72,19 @@ public sealed class DictionaryLabelOverridesController( /// /// 租户删除自己的标签覆盖。 /// - [HttpDelete("tenant/{dictionaryItemId:long}")] + [HttpDelete("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides/{dictionaryItemId:long}")] [PermissionAuthorize("dictionary:override:delete")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> DeleteTenantOverride(long dictionaryItemId, CancellationToken cancellationToken) + public async Task> DeleteTenantOverride(long tenantId, long dictionaryItemId, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 执行删除 var operatorId = currentUserAccessor.UserId; var success = await labelOverrideService.DeleteOverrideAsync( tenantId, @@ -158,19 +156,4 @@ public sealed class DictionaryLabelOverridesController( } #endregion - - private ApiResponse? EnsureTenantHeader() - { - if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader)) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户"); - } - - if (!long.TryParse(tenantHeader.FirstOrDefault(), out _)) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID"); - } - - return null; - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryOverridesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryOverridesController.cs index 43295ee..93c8d96 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryOverridesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryOverridesController.cs @@ -6,7 +6,6 @@ using TakeoutSaaS.Application.Dictionary.Services; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; @@ -16,29 +15,26 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// [ApiVersion("1.0")] [Authorize] -[Route("api/admin/v{version:apiVersion}/dictionary/overrides")] +[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/overrides")] public sealed class DictionaryOverridesController( - DictionaryOverrideService overrideService, - ITenantProvider tenantProvider) + DictionaryOverrideService overrideService) : BaseApiController { - private const string TenantIdHeaderName = "X-Tenant-Id"; - /// /// 获取当前租户的覆盖配置列表。 /// [HttpGet] [PermissionAuthorize("dictionary:override:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List(CancellationToken cancellationToken) + public async Task>> List(long tenantId, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader>(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse>.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 查询覆盖配置 var result = await overrideService.GetOverridesAsync(tenantId, cancellationToken); return ApiResponse>.Ok(result); } @@ -50,15 +46,15 @@ public sealed class DictionaryOverridesController( [PermissionAuthorize("dictionary:override:read")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Detail(string groupCode, CancellationToken cancellationToken) + public async Task> Detail(long tenantId, string groupCode, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 查询覆盖配置 var result = await overrideService.GetOverrideAsync(tenantId, groupCode, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "覆盖配置不存在") @@ -71,15 +67,15 @@ public sealed class DictionaryOverridesController( [HttpPost("{groupCode}/enable")] [PermissionAuthorize("dictionary:override:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Enable(string groupCode, CancellationToken cancellationToken) + public async Task> Enable(long tenantId, string groupCode, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 启用覆盖模式 var result = await overrideService.EnableOverrideAsync(tenantId, groupCode, cancellationToken); return ApiResponse.Ok(result); } @@ -91,15 +87,15 @@ public sealed class DictionaryOverridesController( [PermissionAuthorize("dictionary:override:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Disable(string groupCode, CancellationToken cancellationToken) + public async Task> Disable(long tenantId, string groupCode, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 禁用覆盖模式 var success = await overrideService.DisableOverrideAsync(tenantId, groupCode, cancellationToken); return success ? ApiResponse.Success() @@ -113,17 +109,18 @@ public sealed class DictionaryOverridesController( [PermissionAuthorize("dictionary:override:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> UpdateHiddenItems( + long tenantId, string groupCode, [FromBody] DictionaryOverrideHiddenItemsRequest request, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 更新隐藏项 var result = await overrideService.UpdateHiddenItemsAsync(tenantId, groupCode, request.HiddenItemIds, cancellationToken); return ApiResponse.Ok(result); } @@ -135,33 +132,19 @@ public sealed class DictionaryOverridesController( [PermissionAuthorize("dictionary:override:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> UpdateSortOrder( + long tenantId, string groupCode, [FromBody] DictionaryOverrideSortOrderRequest request, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 更新自定义排序 var result = await overrideService.UpdateCustomSortOrderAsync(tenantId, groupCode, request.SortOrder, cancellationToken); return ApiResponse.Ok(result); } - - private ApiResponse? EnsureTenantHeader() - { - if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader)) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户"); - } - - if (!long.TryParse(tenantHeader.FirstOrDefault(), out _)) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID"); - } - - return null; - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs index df6cf1b..b4aa7c2 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs @@ -8,7 +8,6 @@ using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; @@ -19,10 +18,8 @@ namespace TakeoutSaaS.AdminApi.Controllers; [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/announcements")] -public sealed class TenantAnnouncementsController(IMediator mediator, ITenantContextAccessor tenantContextAccessor) : BaseApiController +public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiController { - private const string TenantIdHeaderName = "X-Tenant-Id"; - /// /// 分页查询公告。 /// @@ -50,21 +47,19 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task>> Search(long tenantId, [FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken) { - if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader)) + // 1. 校验租户标识 + if (tenantId <= 0) { - var request = query with { TenantId = 0 }; - var platformResult = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken)); - return ApiResponse>.Ok(platformResult); + return ApiResponse>.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var headerError = EnsureTenantHeader>(); - if (headerError != null) - { - return headerError; - } + // 2. (空行后) 绑定路由租户并查询列表 + var request = query with { TenantId = tenantId }; - query = query with { TenantId = tenantId }; - var result = await mediator.Send(query, cancellationToken); + // 3. (空行后) 执行查询 + var result = await mediator.Send(request, cancellationToken); + + // 4. (空行后) 返回分页结果 return ApiResponse>.Ok(result); } @@ -96,13 +91,17 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Detail(long tenantId, long announcementId, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } - var result = await mediator.Send(new GetAnnouncementByIdQuery { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken); + // 2. (空行后) 查询公告详情 + var query = new GetAnnouncementByIdQuery { TenantId = tenantId, AnnouncementId = announcementId }; + var result = await mediator.Send(query, cancellationToken); + + // 3. (空行后) 返回详情或 404 return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") : ApiResponse.Ok(result); @@ -146,12 +145,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } + // 2. (空行后) 绑定租户标识并创建公告 command = command with { TenantId = tenantId }; var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); @@ -192,12 +192,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } + // 2. (空行后) 执行更新 command = command with { TenantId = tenantId, AnnouncementId = announcementId }; var result = await mediator.Send(command, cancellationToken); return result is null @@ -236,12 +237,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Publish(long tenantId, long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } + // 2. (空行后) 发布公告 command = command with { AnnouncementId = announcementId }; var result = await mediator.Send(command, cancellationToken); return result is null @@ -280,12 +282,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Revoke(long tenantId, long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } + // 2. (空行后) 撤销公告 command = command with { AnnouncementId = announcementId }; var result = await mediator.Send(command, cancellationToken); return result is null @@ -315,18 +318,19 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Delete(long tenantId, long announcementId, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } + // 2. (空行后) 执行删除 var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken); return ApiResponse.Ok(result); } /// - /// 标记公告已读(兼容旧路径)。 + /// 标记公告已读。 /// /// /// 示例: @@ -351,44 +355,16 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken) { - var headerError = EnsureTenantHeader(); - if (headerError != null) + // 1. 校验租户标识 + if (tenantId <= 0) { - return headerError; + return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); } + // 2. (空行后) 标记已读 var result = await mediator.Send(new MarkAnnouncementAsReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken); return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") : ApiResponse.Ok(result); } - - private ApiResponse? EnsureTenantHeader() - { - if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader)) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户"); - } - - if (!long.TryParse(tenantHeader.FirstOrDefault(), out _)) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID"); - } - - return null; - } - - private async Task ExecuteAsPlatformAsync(Func> action) - { - var original = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(0, null, "platform"); - try - { - return await action(); - } - finally - { - tenantContextAccessor.Current = original; - } - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs index f932a35..d171ab6 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; @@ -18,10 +17,8 @@ namespace TakeoutSaaS.AdminApi.Controllers; [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/roles")] -public sealed class TenantRolesController(IMediator mediator, ITenantProvider tenantProvider) : BaseApiController +public sealed class TenantRolesController(IMediator mediator) : BaseApiController { - private const long PlatformRootTenantId = 1000000000001; - /// /// 租户角色分页。 /// @@ -33,14 +30,7 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te [FromQuery] SearchRolesQuery query, CancellationToken cancellationToken) { - // 1. 校验路由租户与上下文一致(超管租户 1000000000001 放行) - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId) - { - return ApiResponse>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致"); - } - - // 2. 绑定租户并查询角色分页 + // 1. 绑定租户并查询角色分页 var request = new SearchRolesQuery { TenantId = tenantId, @@ -52,7 +42,7 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te }; var result = await mediator.Send(request, cancellationToken); - // 3. 返回分页数据 + // 2. 返回分页数据 return ApiResponse>.Ok(result); } @@ -65,17 +55,10 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail(long tenantId, long roleId, CancellationToken cancellationToken) { - // 1. 校验租户上下文(超管租户 1000000000001 放行) - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户上下文不一致"); - } - - // 2. 查询角色详情 + // 1. 查询角色详情 var result = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken); - // 3. 返回数据或 404 + // 2. 返回数据或 404 return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "角色不存在") : ApiResponse.Ok(result); @@ -92,17 +75,10 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te [FromBody, Required] CreateRoleCommand command, CancellationToken cancellationToken) { - // 1. 校验租户上下文(超管租户 1000000000001 放行) - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户上下文不一致"); - } - - // 2. 创建角色 + // 1. 创建角色 var result = await mediator.Send(command with { TenantId = tenantId }, cancellationToken); - // 3. 返回创建结果 + // 2. 返回创建结果 return ApiResponse.Ok(result); } @@ -119,20 +95,13 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te [FromBody, Required] UpdateRoleCommand command, CancellationToken cancellationToken) { - // 1. 校验租户上下文(超管租户 1000000000001 放行) - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户上下文不一致"); - } - - // 2. 绑定角色 ID + // 1. 绑定角色 ID command = command with { RoleId = roleId, TenantId = tenantId }; - // 3. 执行更新 + // 2. 执行更新 var result = await mediator.Send(command, cancellationToken); - // 4. 返回结果或 404 + // 3. 返回结果或 404 return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "角色不存在") : ApiResponse.Ok(result); @@ -146,18 +115,11 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Delete(long tenantId, long roleId, CancellationToken cancellationToken) { - // 1. 校验租户上下文(超管租户 1000000000001 放行) - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户上下文不一致"); - } - - // 2. 执行删除 + // 1. 执行删除 var command = new DeleteRoleCommand { RoleId = roleId, TenantId = tenantId }; var result = await mediator.Send(command, cancellationToken); - // 3. 返回结果 + // 2. 返回结果 return ApiResponse.Ok(result); } @@ -173,21 +135,14 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te long roleId, CancellationToken cancellationToken) { - // 1. 校验租户上下文(超管租户 1000000000001 放行) - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId) - { - return ApiResponse>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致"); - } - - // 2. 查询角色详情并提取权限 + // 1. 查询角色详情并提取权限 var detail = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken); if (detail is null) { return ApiResponse>.Error(StatusCodes.Status404NotFound, "角色不存在"); } - // 3. 返回权限集合 + // 2. 返回权限集合 return ApiResponse>.Ok(detail.Permissions); } @@ -203,17 +158,10 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te [FromBody, Required] BindRolePermissionsCommand command, CancellationToken cancellationToken) { - // 1. 校验租户上下文(超管租户 1000000000001 放行) - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户上下文不一致"); - } - - // 2. 绑定角色 ID + // 1. 绑定角色 ID command = command with { RoleId = roleId, TenantId = tenantId }; - // 3. 覆盖授权 + // 2. 覆盖授权 var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs index 3108448..b42b168 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs @@ -394,7 +394,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController /// /// 配额校验并占用额度(门店/账号/短信/配送)。 /// - /// 需在请求头携带 X-Tenant-Id 对应的租户。 + /// 租户标识来自路由参数 tenantId,无需强制使用租户请求头。 /// 配额校验结果。 [HttpPost("{tenantId:long}/quotas/check")] [PermissionAuthorize("tenant:quota:check")] diff --git a/src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs b/src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs new file mode 100644 index 0000000..bd30521 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.AdminApi.Filters; + +/// +/// 路由租户上下文过滤器:当路由中包含 tenantId 时,将其注入租户上下文,便于下游使用 与 EF 租户过滤器。 +/// +/// +/// 初始化过滤器。 +/// +/// 租户上下文访问器。 +public sealed class TenantRouteContextFilter(ITenantContextAccessor tenantContextAccessor) : IAsyncActionFilter +{ + /// + /// 在 Action 执行前将路由 tenantId 写入租户上下文,并在结束后恢复。 + /// + /// Action 执行上下文。 + /// 下一个执行委托。 + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // 1. 解析路由 tenantId(仅当 > 0 才视为有效) + if (!TryGetRouteTenantId(context, out var tenantId)) + { + await next(); + return; + } + + // 2. (空行后) 备份并覆盖租户上下文 + var original = tenantContextAccessor.Current; + var current = new TenantContext(tenantId, null, "route:tenantId"); + tenantContextAccessor.Current = current; + context.HttpContext.Items[TenantConstants.HttpContextItemKey] = current; + + // 3. (空行后) 执行后续管道并在 finally 中恢复 + try + { + await next(); + } + finally + { + tenantContextAccessor.Current = original; + + if (original is null) + { + context.HttpContext.Items.Remove(TenantConstants.HttpContextItemKey); + } + else + { + context.HttpContext.Items[TenantConstants.HttpContextItemKey] = original; + } + } + } + + private static bool TryGetRouteTenantId(ActionExecutingContext context, out long tenantId) + { + if (!context.RouteData.Values.TryGetValue("tenantId", out var value) || value is null) + { + tenantId = 0; + return false; + } + + return long.TryParse(value.ToString(), out tenantId) && tenantId > 0; + } +} + diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 5d8837b..449d27b 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -1,11 +1,13 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; using System.Threading.RateLimiting; +using TakeoutSaaS.AdminApi.Filters; using TakeoutSaaS.Application.App.Extensions; using TakeoutSaaS.Application.Identity.Extensions; using TakeoutSaaS.Application.Messaging.Extensions; @@ -61,6 +63,13 @@ if (isDevelopment) }); } +// 4.1 (空行后) 路由租户注入:以路由 tenantId 写入租户上下文 +builder.Services.AddScoped(); +builder.Services.Configure(options => +{ + options.Filters.AddService(); +}); + // 5. 注册领域与基础设施模块 builder.Services.AddIdentityApplication(); builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true);