feat: add admin menu management crud
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<CreateMenuCommand, MenuDefinitionDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MenuDefinitionDto> Handle(CreateMenuCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 构造实体
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var entity = new MenuDefinition
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ParentId = request.ParentId,
|
||||
Name = request.Name.Trim(),
|
||||
Path = request.Path.Trim(),
|
||||
Component = request.Component.Trim(),
|
||||
Title = request.Title.Trim(),
|
||||
Icon = request.Icon?.Trim(),
|
||||
IsIframe = request.IsIframe,
|
||||
Link = string.IsNullOrWhiteSpace(request.Link) ? null : request.Link.Trim(),
|
||||
KeepAlive = request.KeepAlive,
|
||||
SortOrder = request.SortOrder,
|
||||
RequiredPermissions = MenuMapper.JoinCodes(request.RequiredPermissions),
|
||||
MetaPermissions = MenuMapper.JoinCodes(request.MetaPermissions),
|
||||
MetaRoles = MenuMapper.JoinCodes(request.MetaRoles),
|
||||
AuthListJson = request.AuthList.Count == 0
|
||||
? null
|
||||
: System.Text.Json.JsonSerializer.Serialize(request.AuthList)
|
||||
};
|
||||
|
||||
// 2. 持久化
|
||||
await menuRepository.AddAsync(entity, cancellationToken);
|
||||
await menuRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 3. 返回 DTO
|
||||
return MenuMapper.ToDto(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<DeleteMenuCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteMenuCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 删除目标及可能的孤儿由外层保证
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
await menuRepository.DeleteAsync(request.Id, tenantId, cancellationToken);
|
||||
|
||||
// 2. 持久化
|
||||
await menuRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 3. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 菜单列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListMenusQueryHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListMenusQuery, IReadOnlyList<MenuDefinitionDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MenuDefinitionDto>> Handle(ListMenusQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 2. 查询列表
|
||||
var entities = await menuRepository.GetByTenantAsync(tenantId, cancellationToken);
|
||||
|
||||
// 3. 映射 DTO
|
||||
var items = entities.Select(MenuMapper.ToDto).ToList();
|
||||
|
||||
// 4. 返回结果
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 菜单详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class MenuDetailQueryHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<MenuDetailQuery, MenuDefinitionDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MenuDefinitionDto?> Handle(MenuDetailQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 2. 查询实体
|
||||
var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken);
|
||||
if (entity is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 映射并返回
|
||||
return MenuMapper.ToDto(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 菜单映射辅助。
|
||||
/// </summary>
|
||||
internal static class MenuMapper
|
||||
{
|
||||
public static MenuDefinitionDto ToDto(MenuDefinition entity)
|
||||
{
|
||||
// 1. 解析权限字段
|
||||
var requiredPermissions = SplitCodes(entity.RequiredPermissions);
|
||||
var metaPermissions = SplitCodes(entity.MetaPermissions);
|
||||
var metaRoles = SplitCodes(entity.MetaRoles);
|
||||
|
||||
// 2. 解析按钮权限
|
||||
var authList = string.IsNullOrWhiteSpace(entity.AuthListJson)
|
||||
? []
|
||||
: JsonSerializer.Deserialize<List<MenuAuthItemDto>>(entity.AuthListJson) ?? [];
|
||||
|
||||
// 3. 输出 DTO
|
||||
return new MenuDefinitionDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
ParentId = entity.ParentId,
|
||||
Name = entity.Name,
|
||||
Path = entity.Path,
|
||||
Component = entity.Component,
|
||||
Title = entity.Title,
|
||||
Icon = entity.Icon,
|
||||
IsIframe = entity.IsIframe,
|
||||
Link = entity.Link,
|
||||
KeepAlive = entity.KeepAlive,
|
||||
SortOrder = entity.SortOrder,
|
||||
RequiredPermissions = requiredPermissions,
|
||||
MetaPermissions = metaPermissions,
|
||||
MetaRoles = metaRoles,
|
||||
AuthList = authList
|
||||
};
|
||||
}
|
||||
|
||||
public static void FillEntity(MenuDefinition entity, MenuDefinitionDto dto)
|
||||
{
|
||||
// 1. 赋值基础字段
|
||||
entity.ParentId = dto.ParentId;
|
||||
entity.Name = dto.Name;
|
||||
entity.Path = dto.Path;
|
||||
entity.Component = dto.Component;
|
||||
entity.Title = dto.Title;
|
||||
entity.Icon = dto.Icon;
|
||||
entity.IsIframe = dto.IsIframe;
|
||||
entity.Link = dto.Link;
|
||||
entity.KeepAlive = dto.KeepAlive;
|
||||
entity.SortOrder = dto.SortOrder;
|
||||
|
||||
// 2. 权限与按钮
|
||||
entity.RequiredPermissions = JoinCodes(dto.RequiredPermissions);
|
||||
entity.MetaPermissions = JoinCodes(dto.MetaPermissions);
|
||||
entity.MetaRoles = JoinCodes(dto.MetaRoles);
|
||||
entity.AuthListJson = dto.AuthList.Count == 0
|
||||
? null
|
||||
: JsonSerializer.Serialize(dto.AuthList);
|
||||
}
|
||||
|
||||
public static MenuDefinitionDto FromCommand(MenuDefinition? existing, long tenantId, string name, MenuDefinitionDto payload)
|
||||
{
|
||||
// 1. 构造实体
|
||||
var entity = existing ?? new MenuDefinition
|
||||
{
|
||||
TenantId = tenantId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// // 填充字段
|
||||
FillEntity(entity, payload);
|
||||
|
||||
// 2. 返回 DTO 映射
|
||||
return ToDto(entity);
|
||||
}
|
||||
|
||||
public static string JoinCodes(IEnumerable<string> codes)
|
||||
{
|
||||
return string.Join(',', codes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static string[] SplitCodes(string? codes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(codes))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return codes
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<UpdateMenuCommand, MenuDefinitionDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MenuDefinitionDto?> Handle(UpdateMenuCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "菜单不存在");
|
||||
|
||||
// 2. 更新字段
|
||||
entity.ParentId = request.ParentId;
|
||||
entity.Name = request.Name.Trim();
|
||||
entity.Path = request.Path.Trim();
|
||||
entity.Component = request.Component.Trim();
|
||||
entity.Title = request.Title.Trim();
|
||||
entity.Icon = request.Icon?.Trim();
|
||||
entity.IsIframe = request.IsIframe;
|
||||
entity.Link = string.IsNullOrWhiteSpace(request.Link) ? null : request.Link.Trim();
|
||||
entity.KeepAlive = request.KeepAlive;
|
||||
entity.SortOrder = request.SortOrder;
|
||||
entity.RequiredPermissions = MenuMapper.JoinCodes(request.RequiredPermissions);
|
||||
entity.MetaPermissions = MenuMapper.JoinCodes(request.MetaPermissions);
|
||||
entity.MetaRoles = MenuMapper.JoinCodes(request.MetaRoles);
|
||||
entity.AuthListJson = request.AuthList.Count == 0
|
||||
? null
|
||||
: System.Text.Json.JsonSerializer.Serialize(request.AuthList);
|
||||
|
||||
// 3. 持久化
|
||||
await menuRepository.UpdateAsync(entity, cancellationToken);
|
||||
await menuRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 返回 DTO
|
||||
return MenuMapper.ToDto(entity);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user