From 395ac4da05db83be3cd8154c9fa97939d206a120 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Fri, 30 Jan 2026 08:25:12 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=85=8B=E9=9A=86?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E8=A7=92=E8=89=B2=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/admin/v1/tenants/{tenantId}/roles/{roleId}/clone - 支持复制角色基本信息和权限 Co-Authored-By: Claude Opus 4.5 --- .../Controllers/TenantRolesController.cs | 27 ++++++ .../Commands/CloneTenantRoleCommand.cs | 40 +++++++++ .../Handlers/CloneTenantRoleCommandHandler.cs | 86 +++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/CloneTenantRoleCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/CloneTenantRoleCommandHandler.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs index 963929b..f97ceb9 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs @@ -185,4 +185,31 @@ public sealed class TenantRolesController(IMediator mediator) : BaseApiControlle // 3. 返回删除结果 return ApiResponse.Ok(result); } + + /// + /// 克隆租户角色。 + /// + /// 租户 ID。 + /// 源角色 ID。 + /// 克隆命令。 + /// 取消标记。 + /// 克隆后的新角色。 + [HttpPost("{roleId:long}/clone")] + [PermissionAuthorize("identity:role:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(result); + } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CloneTenantRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CloneTenantRoleCommand.cs new file mode 100644 index 0000000..0d23991 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CloneTenantRoleCommand.cs @@ -0,0 +1,40 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 克隆租户角色命令。 +/// +public sealed record CloneTenantRoleCommand : IRequest +{ + /// + /// 租户 ID(由路由绑定)。 + /// + public long TenantId { get; init; } + + /// + /// 源角色 ID(由路由绑定)。 + /// + public long SourceRoleId { get; init; } + + /// + /// 新角色编码(租户内唯一)。 + /// + public string NewCode { get; init; } = string.Empty; + + /// + /// 新角色名称(为空则沿用源角色)。 + /// + public string? Name { get; init; } + + /// + /// 新角色描述(为空则沿用源角色)。 + /// + public string? Description { get; init; } + + /// + /// 是否复制权限(默认 true)。 + /// + public bool CopyPermissions { get; init; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CloneTenantRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CloneTenantRoleCommandHandler.cs new file mode 100644 index 0000000..eb4b9e8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CloneTenantRoleCommandHandler.cs @@ -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; + +/// +/// 克隆租户角色命令处理器。 +/// +public sealed class CloneTenantRoleCommandHandler( + IRoleRepository roleRepository, + IRolePermissionRepository rolePermissionRepository) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +}