fix: 菜单固定为全局

This commit is contained in:
2025-12-27 16:33:18 +08:00
parent 212c7bde44
commit dd58cc2ed0
6 changed files with 61 additions and 32 deletions

View File

@@ -60,11 +60,8 @@ public sealed class MenusController(IMediator mediator) : BaseApiController
[ProducesResponseType(typeof(ApiResponse<MenuDefinitionDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<MenuDefinitionDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<MenuDefinitionDto>> Create([FromBody, Required] CreateMenuCommand command, CancellationToken cancellationToken) public async Task<ApiResponse<MenuDefinitionDto>> Create([FromBody, Required] CreateMenuCommand command, CancellationToken cancellationToken)
{ {
// 1. 创建菜单 // 1. 菜单已固定,禁止新增
var result = await mediator.Send(command, cancellationToken); return await Task.FromResult(ApiResponse<MenuDefinitionDto>.Error(StatusCodes.Status403Forbidden, "菜单已固定,禁止新增"));
// 2. 返回创建结果
return ApiResponse<MenuDefinitionDto>.Ok(result);
} }
/// <summary> /// <summary>
@@ -79,16 +76,8 @@ public sealed class MenusController(IMediator mediator) : BaseApiController
[FromBody, Required] UpdateMenuCommand command, [FromBody, Required] UpdateMenuCommand command,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// 1. 绑定菜单 ID // 1. 菜单已固定,禁止修改
command = command with { Id = menuId }; return await Task.FromResult(ApiResponse<MenuDefinitionDto>.Error(StatusCodes.Status403Forbidden, "菜单已固定,禁止修改"));
// 2. 执行更新
var result = await mediator.Send(command, cancellationToken);
// 3. 返回或 404
return result is null
? ApiResponse<MenuDefinitionDto>.Error(StatusCodes.Status404NotFound, "菜单不存在")
: ApiResponse<MenuDefinitionDto>.Ok(result);
} }
/// <summary> /// <summary>
@@ -99,10 +88,7 @@ public sealed class MenusController(IMediator mediator) : BaseApiController
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
public async Task<ApiResponse<bool>> Delete(long menuId, CancellationToken cancellationToken) public async Task<ApiResponse<bool>> Delete(long menuId, CancellationToken cancellationToken)
{ {
// 1. 删除菜单 // 1. 菜单已固定,禁止删除
var result = await mediator.Send(new DeleteMenuCommand { Id = menuId }, cancellationToken); return await Task.FromResult(ApiResponse<bool>.Error(StatusCodes.Status403Forbidden, "菜单已固定,禁止删除"));
// 2. 返回执行结果
return ApiResponse<bool>.Ok(result);
} }
} }

View File

@@ -1,8 +1,11 @@
using MediatR; using MediatR;
using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Entities;
using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers; namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -18,7 +21,13 @@ public sealed class CreateMenuCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<MenuDefinitionDto> Handle(CreateMenuCommand request, CancellationToken cancellationToken) public async Task<MenuDefinitionDto> Handle(CreateMenuCommand request, CancellationToken cancellationToken)
{ {
// 1. 构造实体 // 1. 菜单固定时禁止新增
if (!MenuPolicy.CanMaintainMenus)
{
throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止新增");
}
// 2. 构造实体
var tenantId = tenantProvider.GetCurrentTenantId(); var tenantId = tenantProvider.GetCurrentTenantId();
var entity = new MenuDefinition var entity = new MenuDefinition
{ {
@@ -41,11 +50,11 @@ public sealed class CreateMenuCommandHandler(
: System.Text.Json.JsonSerializer.Serialize(request.AuthList) : System.Text.Json.JsonSerializer.Serialize(request.AuthList)
}; };
// 2. 持久化 // 3. 持久化
await menuRepository.AddAsync(entity, cancellationToken); await menuRepository.AddAsync(entity, cancellationToken);
await menuRepository.SaveChangesAsync(cancellationToken); await menuRepository.SaveChangesAsync(cancellationToken);
// 3. 返回 DTO // 4. 返回 DTO
return MenuMapper.ToDto(entity); return MenuMapper.ToDto(entity);
} }
} }

View File

@@ -1,6 +1,9 @@
using MediatR; using MediatR;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers; namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -16,14 +19,20 @@ public sealed class DeleteMenuCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> Handle(DeleteMenuCommand request, CancellationToken cancellationToken) public async Task<bool> Handle(DeleteMenuCommand request, CancellationToken cancellationToken)
{ {
// 1. 删除目标及可能的孤儿由外层保证 // 1. 菜单固定时禁止删除
if (!MenuPolicy.CanMaintainMenus)
{
throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止删除");
}
// 2. 删除目标及可能的孤儿由外层保证
var tenantId = tenantProvider.GetCurrentTenantId(); var tenantId = tenantProvider.GetCurrentTenantId();
await menuRepository.DeleteAsync(request.Id, tenantId, cancellationToken); await menuRepository.DeleteAsync(request.Id, tenantId, cancellationToken);
// 2. 持久化 // 3. 持久化
await menuRepository.SaveChangesAsync(cancellationToken); await menuRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果 // 4. 返回执行结果
return true; return true;
} }
} }

View File

@@ -1,4 +1,5 @@
using MediatR; using MediatR;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Domain.Identity.Repositories;
@@ -19,12 +20,18 @@ public sealed class UpdateMenuCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<MenuDefinitionDto?> Handle(UpdateMenuCommand request, CancellationToken cancellationToken) public async Task<MenuDefinitionDto?> Handle(UpdateMenuCommand request, CancellationToken cancellationToken)
{ {
// 1. 校验存在 // 1. 菜单固定时禁止修改
if (!MenuPolicy.CanMaintainMenus)
{
throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止修改");
}
// 2. 校验存在
var tenantId = tenantProvider.GetCurrentTenantId(); var tenantId = tenantProvider.GetCurrentTenantId();
var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken) var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "菜单不存在"); ?? throw new BusinessException(ErrorCodes.NotFound, "菜单不存在");
// 2. 更新字段 // 3. 更新字段
entity.ParentId = request.ParentId; entity.ParentId = request.ParentId;
entity.Name = request.Name.Trim(); entity.Name = request.Name.Trim();
entity.Path = request.Path.Trim(); entity.Path = request.Path.Trim();
@@ -42,11 +49,11 @@ public sealed class UpdateMenuCommandHandler(
? null ? null
: System.Text.Json.JsonSerializer.Serialize(request.AuthList); : System.Text.Json.JsonSerializer.Serialize(request.AuthList);
// 3. 持久化 // 4. 持久化
await menuRepository.UpdateAsync(entity, cancellationToken); await menuRepository.UpdateAsync(entity, cancellationToken);
await menuRepository.SaveChangesAsync(cancellationToken); await menuRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO // 5. 返回 DTO
return MenuMapper.ToDto(entity); return MenuMapper.ToDto(entity);
} }
} }

View File

@@ -0,0 +1,12 @@
namespace TakeoutSaaS.Application.Identity;
/// <summary>
/// 菜单维护策略(固定菜单时禁用增删改)。
/// </summary>
public static class MenuPolicy
{
/// <summary>
/// 是否允许维护菜单(创建/更新/删除)。
/// </summary>
public const bool CanMaintainMenus = false;
}

View File

@@ -10,12 +10,15 @@ namespace TakeoutSaaS.Infrastructure.Identity.Repositories;
/// </summary> /// </summary>
public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuRepository public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuRepository
{ {
private const long PlatformRootTenantId = 1000000000001;
/// <inheritdoc /> /// <inheritdoc />
public Task<IReadOnlyList<MenuDefinition>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default) public Task<IReadOnlyList<MenuDefinition>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default)
{ {
return dbContext.MenuDefinitions return dbContext.MenuDefinitions
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId) .IgnoreQueryFilters()
.Where(x => x.TenantId == PlatformRootTenantId && x.DeletedAt == null)
.OrderBy(x => x.ParentId) .OrderBy(x => x.ParentId)
.ThenBy(x => x.SortOrder) .ThenBy(x => x.SortOrder)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
@@ -27,7 +30,10 @@ public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuReposit
{ {
return dbContext.MenuDefinitions return dbContext.MenuDefinitions
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, cancellationToken); .IgnoreQueryFilters()
.FirstOrDefaultAsync(
x => x.Id == id && x.TenantId == PlatformRootTenantId && x.DeletedAt == null,
cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />