feat: 实现克隆租户角色接口
- POST /api/admin/v1/tenants/{tenantId}/roles/{roleId}/clone
- 支持复制角色基本信息和权限
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -185,4 +185,31 @@ public sealed class TenantRolesController(IMediator mediator) : BaseApiControlle
|
||||
// 3. 返回删除结果
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 克隆租户角色。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="roleId">源角色 ID。</param>
|
||||
/// <param name="command">克隆命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>克隆后的新角色。</returns>
|
||||
[HttpPost("{roleId:long}/clone")]
|
||||
[PermissionAuthorize("identity:role:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<RoleDto>> Clone(
|
||||
long tenantId,
|
||||
long roleId,
|
||||
[FromBody, Required] CloneTenantRoleCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由参数
|
||||
command = command with { TenantId = tenantId, SourceRoleId = roleId };
|
||||
|
||||
// 2. 执行克隆
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回克隆结果
|
||||
return ApiResponse<RoleDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 克隆租户角色命令。
|
||||
/// </summary>
|
||||
public sealed record CloneTenantRoleCommand : IRequest<RoleDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID(由路由绑定)。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 源角色 ID(由路由绑定)。
|
||||
/// </summary>
|
||||
public long SourceRoleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 新角色编码(租户内唯一)。
|
||||
/// </summary>
|
||||
public string NewCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 新角色名称(为空则沿用源角色)。
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 新角色描述(为空则沿用源角色)。
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否复制权限(默认 true)。
|
||||
/// </summary>
|
||||
public bool CopyPermissions { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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 CloneTenantRoleCommandHandler(
|
||||
IRoleRepository roleRepository,
|
||||
IRolePermissionRepository rolePermissionRepository)
|
||||
: IRequestHandler<CloneTenantRoleCommand, RoleDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<RoleDto> Handle(CloneTenantRoleCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验新角色编码不能为空
|
||||
if (string.IsNullOrWhiteSpace(request.NewCode))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "新角色编码不能为空");
|
||||
}
|
||||
|
||||
// 2. 校验源角色存在
|
||||
var source = await roleRepository.FindByIdAsync(PortalType.Tenant, request.TenantId, request.SourceRoleId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "源角色不存在");
|
||||
|
||||
// 3. 校验新角色编码唯一性(租户内)
|
||||
var existing = await roleRepository.FindByCodeAsync(PortalType.Tenant, request.TenantId, request.NewCode, cancellationToken);
|
||||
if (existing is not null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, $"角色编码 {request.NewCode} 已存在");
|
||||
}
|
||||
|
||||
// 4. 构造新角色实体
|
||||
var newRole = new Role
|
||||
{
|
||||
Portal = PortalType.Tenant,
|
||||
TenantId = request.TenantId,
|
||||
Code = request.NewCode.Trim(),
|
||||
Name = string.IsNullOrWhiteSpace(request.Name) ? source.Name : request.Name.Trim(),
|
||||
Description = request.Description ?? source.Description
|
||||
};
|
||||
|
||||
// 5. 持久化新角色
|
||||
await roleRepository.AddAsync(newRole, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 6. 复制权限关系
|
||||
if (request.CopyPermissions)
|
||||
{
|
||||
var sourcePermissions = await rolePermissionRepository.GetByRoleIdsAsync(
|
||||
PortalType.Tenant,
|
||||
request.TenantId,
|
||||
[request.SourceRoleId],
|
||||
cancellationToken);
|
||||
|
||||
var permissionIds = sourcePermissions.Select(p => p.PermissionId).Distinct().ToArray();
|
||||
if (permissionIds.Length > 0)
|
||||
{
|
||||
await rolePermissionRepository.ReplaceRolePermissionsAsync(
|
||||
PortalType.Tenant,
|
||||
request.TenantId,
|
||||
newRole.Id,
|
||||
permissionIds,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 返回 DTO
|
||||
return new RoleDto
|
||||
{
|
||||
Portal = newRole.Portal,
|
||||
Id = newRole.Id,
|
||||
TenantId = newRole.TenantId,
|
||||
Code = newRole.Code,
|
||||
Name = newRole.Name,
|
||||
Description = newRole.Description
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user