refactor: 移除租户兼容并统一路由租户

This commit is contained in:
2026-01-29 04:53:37 +00:00
parent 65c4c00b87
commit d7821fa1af
8 changed files with 186 additions and 221 deletions

View File

@@ -7,7 +7,6 @@ using TakeoutSaaS.Module.Authorization.Attributes;
using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers; namespace TakeoutSaaS.AdminApi.Controllers;
@@ -20,31 +19,29 @@ namespace TakeoutSaaS.AdminApi.Controllers;
[Route("api/admin/v{version:apiVersion}/dictionary/label-overrides")] [Route("api/admin/v{version:apiVersion}/dictionary/label-overrides")]
public sealed class DictionaryLabelOverridesController( public sealed class DictionaryLabelOverridesController(
DictionaryLabelOverrideService labelOverrideService, DictionaryLabelOverrideService labelOverrideService,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor) ICurrentUserAccessor currentUserAccessor)
: BaseApiController : BaseApiController
{ {
private const string TenantIdHeaderName = "X-Tenant-Id";
#region API #region API
/// <summary> /// <summary>
/// 获取当前租户的标签覆盖列表。 /// 获取当前租户的标签覆盖列表。
/// </summary> /// </summary>
[HttpGet("tenant")] [HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides")]
[PermissionAuthorize("dictionary:override:read")] [PermissionAuthorize("dictionary:override:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<LabelOverrideDto>>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<IReadOnlyList<LabelOverrideDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<LabelOverrideDto>>> ListTenantOverrides( public async Task<ApiResponse<IReadOnlyList<LabelOverrideDto>>> ListTenantOverrides(
long tenantId,
[FromQuery] OverrideType? overrideType, [FromQuery] OverrideType? overrideType,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<IReadOnlyList<LabelOverrideDto>>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<IReadOnlyList<LabelOverrideDto>>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 查询租户覆盖列表
var result = await labelOverrideService.GetOverridesAsync(tenantId, overrideType, cancellationToken); var result = await labelOverrideService.GetOverridesAsync(tenantId, overrideType, cancellationToken);
return ApiResponse<IReadOnlyList<LabelOverrideDto>>.Ok(result); return ApiResponse<IReadOnlyList<LabelOverrideDto>>.Ok(result);
} }
@@ -52,20 +49,21 @@ public sealed class DictionaryLabelOverridesController(
/// <summary> /// <summary>
/// 租户覆盖系统字典项的标签。 /// 租户覆盖系统字典项的标签。
/// </summary> /// </summary>
[HttpPost("tenant")] [HttpPost("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides")]
[PermissionAuthorize("dictionary:override:update")] [PermissionAuthorize("dictionary:override:update")]
[ProducesResponseType(typeof(ApiResponse<LabelOverrideDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<LabelOverrideDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<LabelOverrideDto>> CreateTenantOverride( public async Task<ApiResponse<LabelOverrideDto>> CreateTenantOverride(
long tenantId,
[FromBody] UpsertLabelOverrideRequest request, [FromBody] UpsertLabelOverrideRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<LabelOverrideDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<LabelOverrideDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 执行租户覆盖
var operatorId = currentUserAccessor.UserId; var operatorId = currentUserAccessor.UserId;
var result = await labelOverrideService.UpsertTenantOverrideAsync(tenantId, request, operatorId, cancellationToken); var result = await labelOverrideService.UpsertTenantOverrideAsync(tenantId, request, operatorId, cancellationToken);
return ApiResponse<LabelOverrideDto>.Ok(result); return ApiResponse<LabelOverrideDto>.Ok(result);
@@ -74,19 +72,19 @@ public sealed class DictionaryLabelOverridesController(
/// <summary> /// <summary>
/// 租户删除自己的标签覆盖。 /// 租户删除自己的标签覆盖。
/// </summary> /// </summary>
[HttpDelete("tenant/{dictionaryItemId:long}")] [HttpDelete("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides/{dictionaryItemId:long}")]
[PermissionAuthorize("dictionary:override:delete")] [PermissionAuthorize("dictionary:override:delete")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<object>> DeleteTenantOverride(long dictionaryItemId, CancellationToken cancellationToken) public async Task<ApiResponse<object>> DeleteTenantOverride(long tenantId, long dictionaryItemId, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<object>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 执行删除
var operatorId = currentUserAccessor.UserId; var operatorId = currentUserAccessor.UserId;
var success = await labelOverrideService.DeleteOverrideAsync( var success = await labelOverrideService.DeleteOverrideAsync(
tenantId, tenantId,
@@ -158,19 +156,4 @@ public sealed class DictionaryLabelOverridesController(
} }
#endregion #endregion
private ApiResponse<T>? EnsureTenantHeader<T>()
{
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
{
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户");
}
if (!long.TryParse(tenantHeader.FirstOrDefault(), out _))
{
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID");
}
return null;
}
} }

View File

@@ -6,7 +6,6 @@ using TakeoutSaaS.Application.Dictionary.Services;
using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Module.Authorization.Attributes;
using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers; namespace TakeoutSaaS.AdminApi.Controllers;
@@ -16,29 +15,26 @@ namespace TakeoutSaaS.AdminApi.Controllers;
/// </summary> /// </summary>
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Authorize] [Authorize]
[Route("api/admin/v{version:apiVersion}/dictionary/overrides")] [Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/overrides")]
public sealed class DictionaryOverridesController( public sealed class DictionaryOverridesController(
DictionaryOverrideService overrideService, DictionaryOverrideService overrideService)
ITenantProvider tenantProvider)
: BaseApiController : BaseApiController
{ {
private const string TenantIdHeaderName = "X-Tenant-Id";
/// <summary> /// <summary>
/// 获取当前租户的覆盖配置列表。 /// 获取当前租户的覆盖配置列表。
/// </summary> /// </summary>
[HttpGet] [HttpGet]
[PermissionAuthorize("dictionary:override:read")] [PermissionAuthorize("dictionary:override:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<OverrideConfigDto>>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<IReadOnlyList<OverrideConfigDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<OverrideConfigDto>>> List(CancellationToken cancellationToken) public async Task<ApiResponse<IReadOnlyList<OverrideConfigDto>>> List(long tenantId, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<IReadOnlyList<OverrideConfigDto>>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<IReadOnlyList<OverrideConfigDto>>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 查询覆盖配置
var result = await overrideService.GetOverridesAsync(tenantId, cancellationToken); var result = await overrideService.GetOverridesAsync(tenantId, cancellationToken);
return ApiResponse<IReadOnlyList<OverrideConfigDto>>.Ok(result); return ApiResponse<IReadOnlyList<OverrideConfigDto>>.Ok(result);
} }
@@ -50,15 +46,15 @@ public sealed class DictionaryOverridesController(
[PermissionAuthorize("dictionary:override:read")] [PermissionAuthorize("dictionary:override:read")]
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<OverrideConfigDto>> Detail(string groupCode, CancellationToken cancellationToken) public async Task<ApiResponse<OverrideConfigDto>> Detail(long tenantId, string groupCode, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<OverrideConfigDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<OverrideConfigDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 查询覆盖配置
var result = await overrideService.GetOverrideAsync(tenantId, groupCode, cancellationToken); var result = await overrideService.GetOverrideAsync(tenantId, groupCode, cancellationToken);
return result == null return result == null
? ApiResponse<OverrideConfigDto>.Error(ErrorCodes.NotFound, "覆盖配置不存在") ? ApiResponse<OverrideConfigDto>.Error(ErrorCodes.NotFound, "覆盖配置不存在")
@@ -71,15 +67,15 @@ public sealed class DictionaryOverridesController(
[HttpPost("{groupCode}/enable")] [HttpPost("{groupCode}/enable")]
[PermissionAuthorize("dictionary:override:update")] [PermissionAuthorize("dictionary:override:update")]
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<OverrideConfigDto>> Enable(string groupCode, CancellationToken cancellationToken) public async Task<ApiResponse<OverrideConfigDto>> Enable(long tenantId, string groupCode, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<OverrideConfigDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<OverrideConfigDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 启用覆盖模式
var result = await overrideService.EnableOverrideAsync(tenantId, groupCode, cancellationToken); var result = await overrideService.EnableOverrideAsync(tenantId, groupCode, cancellationToken);
return ApiResponse<OverrideConfigDto>.Ok(result); return ApiResponse<OverrideConfigDto>.Ok(result);
} }
@@ -91,15 +87,15 @@ public sealed class DictionaryOverridesController(
[PermissionAuthorize("dictionary:override:update")] [PermissionAuthorize("dictionary:override:update")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<object>> Disable(string groupCode, CancellationToken cancellationToken) public async Task<ApiResponse<object>> Disable(long tenantId, string groupCode, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<object>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 禁用覆盖模式
var success = await overrideService.DisableOverrideAsync(tenantId, groupCode, cancellationToken); var success = await overrideService.DisableOverrideAsync(tenantId, groupCode, cancellationToken);
return success return success
? ApiResponse.Success() ? ApiResponse.Success()
@@ -113,17 +109,18 @@ public sealed class DictionaryOverridesController(
[PermissionAuthorize("dictionary:override:update")] [PermissionAuthorize("dictionary:override:update")]
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<OverrideConfigDto>> UpdateHiddenItems( public async Task<ApiResponse<OverrideConfigDto>> UpdateHiddenItems(
long tenantId,
string groupCode, string groupCode,
[FromBody] DictionaryOverrideHiddenItemsRequest request, [FromBody] DictionaryOverrideHiddenItemsRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<OverrideConfigDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<OverrideConfigDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 更新隐藏项
var result = await overrideService.UpdateHiddenItemsAsync(tenantId, groupCode, request.HiddenItemIds, cancellationToken); var result = await overrideService.UpdateHiddenItemsAsync(tenantId, groupCode, request.HiddenItemIds, cancellationToken);
return ApiResponse<OverrideConfigDto>.Ok(result); return ApiResponse<OverrideConfigDto>.Ok(result);
} }
@@ -135,33 +132,19 @@ public sealed class DictionaryOverridesController(
[PermissionAuthorize("dictionary:override:update")] [PermissionAuthorize("dictionary:override:update")]
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<OverrideConfigDto>> UpdateSortOrder( public async Task<ApiResponse<OverrideConfigDto>> UpdateSortOrder(
long tenantId,
string groupCode, string groupCode,
[FromBody] DictionaryOverrideSortOrderRequest request, [FromBody] DictionaryOverrideSortOrderRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<OverrideConfigDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<OverrideConfigDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
var tenantId = tenantProvider.GetCurrentTenantId(); // 2. (空行后) 更新自定义排序
var result = await overrideService.UpdateCustomSortOrderAsync(tenantId, groupCode, request.SortOrder, cancellationToken); var result = await overrideService.UpdateCustomSortOrderAsync(tenantId, groupCode, request.SortOrder, cancellationToken);
return ApiResponse<OverrideConfigDto>.Ok(result); return ApiResponse<OverrideConfigDto>.Ok(result);
} }
private ApiResponse<T>? EnsureTenantHeader<T>()
{
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
{
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户");
}
if (!long.TryParse(tenantHeader.FirstOrDefault(), out _))
{
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID");
}
return null;
}
} }

View File

@@ -8,7 +8,6 @@ using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Module.Authorization.Attributes;
using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers; namespace TakeoutSaaS.AdminApi.Controllers;
@@ -19,10 +18,8 @@ namespace TakeoutSaaS.AdminApi.Controllers;
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Authorize] [Authorize]
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/announcements")] [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";
/// <summary> /// <summary>
/// 分页查询公告。 /// 分页查询公告。
/// </summary> /// </summary>
@@ -50,21 +47,19 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> Search(long tenantId, [FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken) public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> 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 }; return ApiResponse<PagedResult<TenantAnnouncementDto>>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
var platformResult = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(platformResult);
} }
var headerError = EnsureTenantHeader<PagedResult<TenantAnnouncementDto>>(); // 2. (空行后) 绑定路由租户并查询列表
if (headerError != null) var request = query with { TenantId = tenantId };
{
return headerError;
}
query = query with { TenantId = tenantId }; // 3. (空行后) 执行查询
var result = await mediator.Send(query, cancellationToken); var result = await mediator.Send(request, cancellationToken);
// 4. (空行后) 返回分页结果
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result); return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
} }
@@ -96,13 +91,17 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long tenantId, long announcementId, CancellationToken cancellationToken) public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long tenantId, long announcementId, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<TenantAnnouncementDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<TenantAnnouncementDto>.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 return result is null
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在") ? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
: ApiResponse<TenantAnnouncementDto>.Ok(result); : ApiResponse<TenantAnnouncementDto>.Ok(result);
@@ -146,12 +145,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<TenantAnnouncementDto>> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken) public async Task<ApiResponse<TenantAnnouncementDto>> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<TenantAnnouncementDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
// 2. (空行后) 绑定租户标识并创建公告
command = command with { TenantId = tenantId }; command = command with { TenantId = tenantId };
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
return ApiResponse<TenantAnnouncementDto>.Ok(result); return ApiResponse<TenantAnnouncementDto>.Ok(result);
@@ -192,12 +192,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<TenantAnnouncementDto>> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken) public async Task<ApiResponse<TenantAnnouncementDto>> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<TenantAnnouncementDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
// 2. (空行后) 执行更新
command = command with { TenantId = tenantId, AnnouncementId = announcementId }; command = command with { TenantId = tenantId, AnnouncementId = announcementId };
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
return result is null return result is null
@@ -236,12 +237,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<TenantAnnouncementDto>> Publish(long tenantId, long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken) public async Task<ApiResponse<TenantAnnouncementDto>> Publish(long tenantId, long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<TenantAnnouncementDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
// 2. (空行后) 发布公告
command = command with { AnnouncementId = announcementId }; command = command with { AnnouncementId = announcementId };
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
return result is null return result is null
@@ -280,12 +282,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<TenantAnnouncementDto>> Revoke(long tenantId, long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken) public async Task<ApiResponse<TenantAnnouncementDto>> Revoke(long tenantId, long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<TenantAnnouncementDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
// 2. (空行后) 撤销公告
command = command with { AnnouncementId = announcementId }; command = command with { AnnouncementId = announcementId };
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
return result is null return result is null
@@ -315,18 +318,19 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<bool>> Delete(long tenantId, long announcementId, CancellationToken cancellationToken) public async Task<ApiResponse<bool>> Delete(long tenantId, long announcementId, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<bool>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
// 2. (空行后) 执行删除
var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken); var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
return ApiResponse<bool>.Ok(result); return ApiResponse<bool>.Ok(result);
} }
/// <summary> /// <summary>
/// 标记公告已读(兼容旧路径) /// 标记公告已读。
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// 示例: /// 示例:
@@ -351,44 +355,16 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken) public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken)
{ {
var headerError = EnsureTenantHeader<TenantAnnouncementDto>(); // 1. 校验租户标识
if (headerError != null) if (tenantId <= 0)
{ {
return headerError; return ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
} }
// 2. (空行后) 标记已读
var result = await mediator.Send(new MarkAnnouncementAsReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken); var result = await mediator.Send(new MarkAnnouncementAsReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
return result is null return result is null
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在") ? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
: ApiResponse<TenantAnnouncementDto>.Ok(result); : ApiResponse<TenantAnnouncementDto>.Ok(result);
} }
private ApiResponse<T>? EnsureTenantHeader<T>()
{
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
{
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户");
}
if (!long.TryParse(tenantHeader.FirstOrDefault(), out _))
{
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID");
}
return null;
}
private async Task<T> ExecuteAsPlatformAsync<T>(Func<Task<T>> action)
{
var original = tenantContextAccessor.Current;
tenantContextAccessor.Current = new TenantContext(0, null, "platform");
try
{
return await action();
}
finally
{
tenantContextAccessor.Current = original;
}
}
} }

View File

@@ -7,7 +7,6 @@ using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Application.Identity.Queries;
using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Module.Authorization.Attributes;
using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers; namespace TakeoutSaaS.AdminApi.Controllers;
@@ -18,10 +17,8 @@ namespace TakeoutSaaS.AdminApi.Controllers;
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Authorize] [Authorize]
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/roles")] [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;
/// <summary> /// <summary>
/// 租户角色分页。 /// 租户角色分页。
/// </summary> /// </summary>
@@ -33,14 +30,7 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
[FromQuery] SearchRolesQuery query, [FromQuery] SearchRolesQuery query,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// 1. 校验路由租户与上下文一致(超管租户 1000000000001 放行) // 1. 绑定租户并查询角色分页
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
{
return ApiResponse<PagedResult<RoleDto>>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 绑定租户并查询角色分页
var request = new SearchRolesQuery var request = new SearchRolesQuery
{ {
TenantId = tenantId, TenantId = tenantId,
@@ -52,7 +42,7 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
}; };
var result = await mediator.Send(request, cancellationToken); var result = await mediator.Send(request, cancellationToken);
// 3. 返回分页数据 // 2. 返回分页数据
return ApiResponse<PagedResult<RoleDto>>.Ok(result); return ApiResponse<PagedResult<RoleDto>>.Ok(result);
} }
@@ -65,17 +55,10 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
[ProducesResponseType(typeof(ApiResponse<RoleDetailDto>), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse<RoleDetailDto>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<RoleDetailDto>> Detail(long tenantId, long roleId, CancellationToken cancellationToken) public async Task<ApiResponse<RoleDetailDto>> Detail(long tenantId, long roleId, CancellationToken cancellationToken)
{ {
// 1. 校验租户上下文(超管租户 1000000000001 放行) // 1. 查询角色详情
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
{
return ApiResponse<RoleDetailDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 查询角色详情
var result = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken); var result = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken);
// 3. 返回数据或 404 // 2. 返回数据或 404
return result is null return result is null
? ApiResponse<RoleDetailDto>.Error(StatusCodes.Status404NotFound, "角色不存在") ? ApiResponse<RoleDetailDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
: ApiResponse<RoleDetailDto>.Ok(result); : ApiResponse<RoleDetailDto>.Ok(result);
@@ -92,17 +75,10 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
[FromBody, Required] CreateRoleCommand command, [FromBody, Required] CreateRoleCommand command,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// 1. 校验租户上下文(超管租户 1000000000001 放行) // 1. 创建角色
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
{
return ApiResponse<RoleDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 创建角色
var result = await mediator.Send(command with { TenantId = tenantId }, cancellationToken); var result = await mediator.Send(command with { TenantId = tenantId }, cancellationToken);
// 3. 返回创建结果 // 2. 返回创建结果
return ApiResponse<RoleDto>.Ok(result); return ApiResponse<RoleDto>.Ok(result);
} }
@@ -119,20 +95,13 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
[FromBody, Required] UpdateRoleCommand command, [FromBody, Required] UpdateRoleCommand command,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// 1. 校验租户上下文(超管租户 1000000000001 放行) // 1. 绑定角色 ID
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
{
return ApiResponse<RoleDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 绑定角色 ID
command = command with { RoleId = roleId, TenantId = tenantId }; command = command with { RoleId = roleId, TenantId = tenantId };
// 3. 执行更新 // 2. 执行更新
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
// 4. 返回结果或 404 // 3. 返回结果或 404
return result is null return result is null
? ApiResponse<RoleDto>.Error(StatusCodes.Status404NotFound, "角色不存在") ? ApiResponse<RoleDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
: ApiResponse<RoleDto>.Ok(result); : ApiResponse<RoleDto>.Ok(result);
@@ -146,18 +115,11 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
public async Task<ApiResponse<bool>> Delete(long tenantId, long roleId, CancellationToken cancellationToken) public async Task<ApiResponse<bool>> Delete(long tenantId, long roleId, CancellationToken cancellationToken)
{ {
// 1. 校验租户上下文(超管租户 1000000000001 放行) // 1. 执行删除
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
{
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 执行删除
var command = new DeleteRoleCommand { RoleId = roleId, TenantId = tenantId }; var command = new DeleteRoleCommand { RoleId = roleId, TenantId = tenantId };
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
// 3. 返回结果 // 2. 返回结果
return ApiResponse<bool>.Ok(result); return ApiResponse<bool>.Ok(result);
} }
@@ -173,21 +135,14 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
long roleId, long roleId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// 1. 校验租户上下文(超管租户 1000000000001 放行) // 1. 查询角色详情并提取权限
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
{
return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 查询角色详情并提取权限
var detail = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken); var detail = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken);
if (detail is null) if (detail is null)
{ {
return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status404NotFound, "角色不存在"); return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status404NotFound, "角色不存在");
} }
// 3. 返回权限集合 // 2. 返回权限集合
return ApiResponse<IReadOnlyList<PermissionDto>>.Ok(detail.Permissions); return ApiResponse<IReadOnlyList<PermissionDto>>.Ok(detail.Permissions);
} }
@@ -203,17 +158,10 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
[FromBody, Required] BindRolePermissionsCommand command, [FromBody, Required] BindRolePermissionsCommand command,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// 1. 校验租户上下文(超管租户 1000000000001 放行) // 1. 绑定角色 ID
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
{
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 绑定角色 ID
command = command with { RoleId = roleId, TenantId = tenantId }; command = command with { RoleId = roleId, TenantId = tenantId };
// 3. 覆盖授权 // 2. 覆盖授权
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
return ApiResponse<bool>.Ok(result); return ApiResponse<bool>.Ok(result);
} }

View File

@@ -394,7 +394,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
/// <summary> /// <summary>
/// 配额校验并占用额度(门店/账号/短信/配送)。 /// 配额校验并占用额度(门店/账号/短信/配送)。
/// </summary> /// </summary>
/// <remarks>需在请求头携带 X-Tenant-Id 对应的租户。</remarks> /// <remarks>租户标识来自路由参数 tenantId,无需强制使用租户请求头。</remarks>
/// <returns>配额校验结果。</returns> /// <returns>配额校验结果。</returns>
[HttpPost("{tenantId:long}/quotas/check")] [HttpPost("{tenantId:long}/quotas/check")]
[PermissionAuthorize("tenant:quota:check")] [PermissionAuthorize("tenant:quota:check")]

View File

@@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Mvc.Filters;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.AdminApi.Filters;
/// <summary>
/// 路由租户上下文过滤器:当路由中包含 tenantId 时,将其注入租户上下文,便于下游使用 <see cref="ITenantProvider"/> 与 EF 租户过滤器。
/// </summary>
/// <remarks>
/// 初始化过滤器。
/// </remarks>
/// <param name="tenantContextAccessor">租户上下文访问器。</param>
public sealed class TenantRouteContextFilter(ITenantContextAccessor tenantContextAccessor) : IAsyncActionFilter
{
/// <summary>
/// 在 Action 执行前将路由 tenantId 写入租户上下文,并在结束后恢复。
/// </summary>
/// <param name="context">Action 执行上下文。</param>
/// <param name="next">下一个执行委托。</param>
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;
}
}

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using OpenTelemetry.Metrics; using OpenTelemetry.Metrics;
using OpenTelemetry.Resources; using OpenTelemetry.Resources;
using OpenTelemetry.Trace; using OpenTelemetry.Trace;
using Serilog; using Serilog;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using TakeoutSaaS.AdminApi.Filters;
using TakeoutSaaS.Application.App.Extensions; using TakeoutSaaS.Application.App.Extensions;
using TakeoutSaaS.Application.Identity.Extensions; using TakeoutSaaS.Application.Identity.Extensions;
using TakeoutSaaS.Application.Messaging.Extensions; using TakeoutSaaS.Application.Messaging.Extensions;
@@ -61,6 +63,13 @@ if (isDevelopment)
}); });
} }
// 4.1 (空行后) 路由租户注入:以路由 tenantId 写入租户上下文
builder.Services.AddScoped<TenantRouteContextFilter>();
builder.Services.Configure<MvcOptions>(options =>
{
options.Filters.AddService<TenantRouteContextFilter>();
});
// 5. 注册领域与基础设施模块 // 5. 注册领域与基础设施模块
builder.Services.AddIdentityApplication(); builder.Services.AddIdentityApplication();
builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true); builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true);