refactor: 移除租户兼容并统一路由租户
This commit is contained in:
@@ -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(租户覆盖系统字典)
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前租户的标签覆盖列表。
|
||||
/// </summary>
|
||||
[HttpGet("tenant")]
|
||||
[HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides")]
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<LabelOverrideDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<LabelOverrideDto>>> ListTenantOverrides(
|
||||
long tenantId,
|
||||
[FromQuery] OverrideType? overrideType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<IReadOnlyList<LabelOverrideDto>>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
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);
|
||||
return ApiResponse<IReadOnlyList<LabelOverrideDto>>.Ok(result);
|
||||
}
|
||||
@@ -52,20 +49,21 @@ public sealed class DictionaryLabelOverridesController(
|
||||
/// <summary>
|
||||
/// 租户覆盖系统字典项的标签。
|
||||
/// </summary>
|
||||
[HttpPost("tenant")]
|
||||
[HttpPost("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/dictionary/label-overrides")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<LabelOverrideDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<LabelOverrideDto>> CreateTenantOverride(
|
||||
long tenantId,
|
||||
[FromBody] UpsertLabelOverrideRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<LabelOverrideDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<LabelOverrideDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 执行租户覆盖
|
||||
var operatorId = currentUserAccessor.UserId;
|
||||
var result = await labelOverrideService.UpsertTenantOverrideAsync(tenantId, request, operatorId, cancellationToken);
|
||||
return ApiResponse<LabelOverrideDto>.Ok(result);
|
||||
@@ -74,19 +72,19 @@ public sealed class DictionaryLabelOverridesController(
|
||||
/// <summary>
|
||||
/// 租户删除自己的标签覆盖。
|
||||
/// </summary>
|
||||
[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<object>), StatusCodes.Status200OK)]
|
||||
[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>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<object>.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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
[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";
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前租户的覆盖配置列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[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>>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<IReadOnlyList<OverrideConfigDto>>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 查询覆盖配置
|
||||
var result = await overrideService.GetOverridesAsync(tenantId, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<OverrideConfigDto>>.Ok(result);
|
||||
}
|
||||
@@ -50,15 +46,15 @@ public sealed class DictionaryOverridesController(
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
[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>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<OverrideConfigDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 查询覆盖配置
|
||||
var result = await overrideService.GetOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<OverrideConfigDto>.Error(ErrorCodes.NotFound, "覆盖配置不存在")
|
||||
@@ -71,15 +67,15 @@ public sealed class DictionaryOverridesController(
|
||||
[HttpPost("{groupCode}/enable")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[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>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<OverrideConfigDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 启用覆盖模式
|
||||
var result = await overrideService.EnableOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
@@ -91,15 +87,15 @@ public sealed class DictionaryOverridesController(
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[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>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<object>.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<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> UpdateHiddenItems(
|
||||
long tenantId,
|
||||
string groupCode,
|
||||
[FromBody] DictionaryOverrideHiddenItemsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
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);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
@@ -135,33 +132,19 @@ public sealed class DictionaryOverridesController(
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> UpdateSortOrder(
|
||||
long tenantId,
|
||||
string groupCode,
|
||||
[FromBody] DictionaryOverrideSortOrderRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询公告。
|
||||
/// </summary>
|
||||
@@ -50,21 +47,19 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
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 };
|
||||
var platformResult = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(platformResult);
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
var headerError = EnsureTenantHeader<PagedResult<TenantAnnouncementDto>>();
|
||||
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<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
@@ -96,13 +91,17 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
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
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
@@ -146,12 +145,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
// 2. (空行后) 绑定租户标识并创建公告
|
||||
command = command with { TenantId = tenantId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
@@ -192,12 +192,13 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<TenantAnnouncementDto>.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<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Publish(long tenantId, long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<TenantAnnouncementDto>.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<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Revoke(long tenantId, long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<TenantAnnouncementDto>.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<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<bool>> Delete(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<bool>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行删除
|
||||
var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记公告已读(兼容旧路径)。
|
||||
/// 标记公告已读。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
@@ -351,44 +355,16 @@ public sealed class TenantAnnouncementsController(IMediator mediator, ITenantCon
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
// 1. 校验租户标识
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
return headerError;
|
||||
return ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status400BadRequest, "租户标识无效");
|
||||
}
|
||||
|
||||
// 2. (空行后) 标记已读
|
||||
var result = await mediator.Send(new MarkAnnouncementAsReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 租户角色分页。
|
||||
/// </summary>
|
||||
@@ -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<PagedResult<RoleDto>>.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<PagedResult<RoleDto>>.Ok(result);
|
||||
}
|
||||
|
||||
@@ -65,17 +55,10 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<RoleDetailDto>> Detail(long tenantId, long roleId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<RoleDetailDto>.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<RoleDetailDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
|
||||
: ApiResponse<RoleDetailDto>.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<RoleDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 创建角色
|
||||
// 1. 创建角色
|
||||
var result = await mediator.Send(command with { TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 3. 返回创建结果
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<RoleDto>.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<RoleDto>.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<RoleDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
|
||||
: ApiResponse<RoleDto>.Ok(result);
|
||||
@@ -146,18 +115,11 @@ public sealed class TenantRolesController(IMediator mediator, ITenantProvider te
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(long tenantId, long roleId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 执行删除
|
||||
// 1. 执行删除
|
||||
var command = new DeleteRoleCommand { RoleId = roleId, TenantId = tenantId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果
|
||||
// 2. 返回结果
|
||||
return ApiResponse<bool>.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<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 查询角色详情并提取权限
|
||||
// 1. 查询角色详情并提取权限
|
||||
var detail = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken);
|
||||
if (detail is null)
|
||||
{
|
||||
return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status404NotFound, "角色不存在");
|
||||
}
|
||||
|
||||
// 3. 返回权限集合
|
||||
// 2. 返回权限集合
|
||||
return ApiResponse<IReadOnlyList<PermissionDto>>.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<bool>.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<bool>.Ok(result);
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
||||
/// <summary>
|
||||
/// 配额校验并占用额度(门店/账号/短信/配送)。
|
||||
/// </summary>
|
||||
/// <remarks>需在请求头携带 X-Tenant-Id 对应的租户。</remarks>
|
||||
/// <remarks>租户标识来自路由参数 tenantId,无需强制使用租户请求头。</remarks>
|
||||
/// <returns>配额校验结果。</returns>
|
||||
[HttpPost("{tenantId:long}/quotas/check")]
|
||||
[PermissionAuthorize("tenant:quota:check")]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TenantRouteContextFilter>();
|
||||
builder.Services.Configure<MvcOptions>(options =>
|
||||
{
|
||||
options.Filters.AddService<TenantRouteContextFilter>();
|
||||
});
|
||||
|
||||
// 5. 注册领域与基础设施模块
|
||||
builder.Services.AddIdentityApplication();
|
||||
builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true);
|
||||
|
||||
Reference in New Issue
Block a user