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:
2026-01-30 08:25:12 +00:00
parent 764661f9e3
commit 395ac4da05
3 changed files with 153 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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
};
}
}