feat: 实现租户角色管理 API
- 创建角色:POST /api/admin/v1/tenants/{tenantId}/roles
- 更新角色:PUT /api/admin/v1/tenants/{tenantId}/roles/{roleId}
- 获取角色权限:GET /api/admin/v1/tenants/{tenantId}/roles/{roleId}/permissions
- 更新角色权限:PUT /api/admin/v1/tenants/{tenantId}/roles/{roleId}/permissions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建租户角色命令。
|
||||
/// </summary>
|
||||
public sealed record CreateTenantRoleCommand : IRequest<RoleDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID(由路由绑定)。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色编码(租户内唯一)。
|
||||
/// </summary>
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色描述。
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新租户角色命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateTenantRoleCommand : IRequest<RoleDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID(由路由绑定)。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID(由路由绑定)。
|
||||
/// </summary>
|
||||
public long RoleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色描述。
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新租户角色权限命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateTenantRolePermissionsCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID(由路由绑定)。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID(由路由绑定)。
|
||||
/// </summary>
|
||||
public long RoleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限 ID 集合(替换模式)。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<long> PermissionIds { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建租户角色命令处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateTenantRoleCommandHandler(IRoleRepository roleRepository)
|
||||
: IRequestHandler<CreateTenantRoleCommand, RoleDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<RoleDto> Handle(CreateTenantRoleCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验必填字段
|
||||
if (string.IsNullOrWhiteSpace(request.Code) || string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "角色编码与名称不能为空");
|
||||
}
|
||||
|
||||
// 2. 检查编码唯一性(租户内)
|
||||
var existing = await roleRepository.FindByCodeAsync(PortalType.Tenant, request.TenantId, request.Code, cancellationToken);
|
||||
if (existing is not null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, $"角色编码 {request.Code} 已存在");
|
||||
}
|
||||
|
||||
// 3. 构建角色实体
|
||||
var role = new Role
|
||||
{
|
||||
Portal = PortalType.Tenant,
|
||||
TenantId = request.TenantId,
|
||||
Code = request.Code.Trim(),
|
||||
Name = request.Name.Trim(),
|
||||
Description = request.Description
|
||||
};
|
||||
|
||||
// 4. 持久化
|
||||
await roleRepository.AddAsync(role, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return new RoleDto
|
||||
{
|
||||
Portal = role.Portal,
|
||||
Id = role.Id,
|
||||
TenantId = role.TenantId,
|
||||
Code = role.Code,
|
||||
Name = role.Name,
|
||||
Description = role.Description
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 获取租户角色权限列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetTenantRolePermissionsQueryHandler(
|
||||
IRoleRepository roleRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<GetTenantRolePermissionsQuery, IReadOnlyList<PermissionDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PermissionDto>> Handle(GetTenantRolePermissionsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验角色存在
|
||||
var role = await roleRepository.FindByIdAsync(PortalType.Tenant, request.TenantId, request.RoleId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "角色不存在");
|
||||
|
||||
// 2. 查询角色权限关系
|
||||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(
|
||||
PortalType.Tenant,
|
||||
request.TenantId,
|
||||
[request.RoleId],
|
||||
cancellationToken);
|
||||
|
||||
// 3. 提取权限 ID 集合
|
||||
var permissionIds = rolePermissions.Select(rp => rp.PermissionId).Distinct().ToArray();
|
||||
if (permissionIds.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// 4. 查询权限详情
|
||||
var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
|
||||
// 5. 映射 DTO
|
||||
return permissions
|
||||
.Select(p => new PermissionDto
|
||||
{
|
||||
Portal = p.Portal,
|
||||
Id = p.Id,
|
||||
ParentId = p.ParentId,
|
||||
SortOrder = p.SortOrder,
|
||||
Type = p.Type,
|
||||
Name = p.Name,
|
||||
Code = p.Code,
|
||||
Description = p.Description
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新租户角色命令处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateTenantRoleCommandHandler(IRoleRepository roleRepository)
|
||||
: IRequestHandler<UpdateTenantRoleCommand, RoleDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<RoleDto?> Handle(UpdateTenantRoleCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验必填字段
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "角色名称不能为空");
|
||||
}
|
||||
|
||||
// 2. 查询目标角色
|
||||
var role = await roleRepository.FindByIdAsync(PortalType.Tenant, request.TenantId, request.RoleId, cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
role.Name = request.Name.Trim();
|
||||
role.Description = request.Description;
|
||||
|
||||
// 4. 持久化
|
||||
await roleRepository.UpdateAsync(role, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return new RoleDto
|
||||
{
|
||||
Portal = role.Portal,
|
||||
Id = role.Id,
|
||||
TenantId = role.TenantId,
|
||||
Code = role.Code,
|
||||
Name = role.Name,
|
||||
Description = role.Description
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新租户角色权限命令处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateTenantRolePermissionsCommandHandler(
|
||||
IRoleRepository roleRepository,
|
||||
IRolePermissionRepository rolePermissionRepository)
|
||||
: IRequestHandler<UpdateTenantRolePermissionsCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(UpdateTenantRolePermissionsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验角色存在
|
||||
var role = await roleRepository.FindByIdAsync(PortalType.Tenant, request.TenantId, request.RoleId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "角色不存在");
|
||||
|
||||
// 2. 替换角色权限(事务保证)
|
||||
await rolePermissionRepository.ReplaceRolePermissionsAsync(
|
||||
PortalType.Tenant,
|
||||
request.TenantId,
|
||||
request.RoleId,
|
||||
request.PermissionIds,
|
||||
cancellationToken);
|
||||
|
||||
// 3. 返回成功
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取租户角色权限列表查询。
|
||||
/// </summary>
|
||||
public sealed record GetTenantRolePermissionsQuery : IRequest<IReadOnlyList<PermissionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID。
|
||||
/// </summary>
|
||||
public long RoleId { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user