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. 返回删除结果
|
// 3. 返回删除结果
|
||||||
return ApiResponse<bool>.Ok(result);
|
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