From 2dbd004ce0289a08aa773c208a0342c4d3006374 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Fri, 30 Jan 2026 07:48:35 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E7=AE=A1=E7=90=86=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建角色: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 --- .../Controllers/TenantRolesController.cs | 128 ++++++++++++++++++ .../Commands/CreateTenantRoleCommand.cs | 30 ++++ .../Commands/UpdateTenantRoleCommand.cs | 30 ++++ .../UpdateTenantRolePermissionsCommand.cs | 24 ++++ .../CreateTenantRoleCommandHandler.cs | 59 ++++++++ .../GetTenantRolePermissionsQueryHandler.cs | 59 ++++++++ .../UpdateTenantRoleCommandHandler.cs | 52 +++++++ ...dateTenantRolePermissionsCommandHandler.cs | 36 +++++ .../Queries/GetTenantRolePermissionsQuery.cs | 20 +++ 9 files changed, 438 insertions(+) create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/CreateTenantRoleCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRoleCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRolePermissionsCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateTenantRoleCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/GetTenantRolePermissionsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRoleCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRolePermissionsCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/GetTenantRolePermissionsQuery.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs new file mode 100644 index 0000000..4cc8315 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantRolesController.cs @@ -0,0 +1,128 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 租户角色管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/roles")] +public sealed class TenantRolesController(IMediator mediator) : BaseApiController +{ + /// + /// 创建租户角色。 + /// + /// 租户 ID。 + /// 创建命令。 + /// 取消标记。 + /// 创建后的角色。 + [HttpPost] + [PermissionAuthorize("identity:role:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create( + long tenantId, + [FromBody, Required] CreateTenantRoleCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定租户 ID + command = command with { TenantId = tenantId }; + + // 2. 执行创建 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回创建结果 + return ApiResponse.Ok(result); + } + + /// + /// 更新租户角色。 + /// + /// 租户 ID。 + /// 角色 ID。 + /// 更新命令。 + /// 取消标记。 + /// 更新后的角色。 + [HttpPut("{roleId:long}")] + [PermissionAuthorize("identity:role:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update( + long tenantId, + long roleId, + [FromBody, Required] UpdateTenantRoleCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定路由参数 + command = command with { TenantId = tenantId, RoleId = roleId }; + + // 2. 执行更新 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回更新结果或 404 + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "角色不存在") + : ApiResponse.Ok(result); + } + + /// + /// 获取租户角色权限列表。 + /// + /// 租户 ID。 + /// 角色 ID。 + /// 取消标记。 + /// 权限集合。 + [HttpGet("{roleId:long}/permissions")] + [PermissionAuthorize("identity:role:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetPermissions( + long tenantId, + long roleId, + CancellationToken cancellationToken) + { + // 1. 构造查询 + var query = new GetTenantRolePermissionsQuery { TenantId = tenantId, RoleId = roleId }; + + // 2. 执行查询 + var result = await mediator.Send(query, cancellationToken); + + // 3. 返回权限集合 + return ApiResponse>.Ok(result); + } + + /// + /// 更新租户角色权限。 + /// + /// 租户 ID。 + /// 角色 ID。 + /// 更新命令。 + /// 取消标记。 + /// 更新结果。 + [HttpPut("{roleId:long}/permissions")] + [PermissionAuthorize("identity:role:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> UpdatePermissions( + long tenantId, + long roleId, + [FromBody, Required] UpdateTenantRolePermissionsCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定路由参数 + command = command with { TenantId = tenantId, RoleId = roleId }; + + // 2. 执行更新 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回更新结果 + return ApiResponse.Ok(result); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateTenantRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateTenantRoleCommand.cs new file mode 100644 index 0000000..8b02b65 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateTenantRoleCommand.cs @@ -0,0 +1,30 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 创建租户角色命令。 +/// +public sealed record CreateTenantRoleCommand : IRequest +{ + /// + /// 租户 ID(由路由绑定)。 + /// + public long TenantId { get; init; } + + /// + /// 角色编码(租户内唯一)。 + /// + public string Code { get; init; } = string.Empty; + + /// + /// 角色名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 角色描述。 + /// + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRoleCommand.cs new file mode 100644 index 0000000..540d8f7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRoleCommand.cs @@ -0,0 +1,30 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 更新租户角色命令。 +/// +public sealed record UpdateTenantRoleCommand : IRequest +{ + /// + /// 租户 ID(由路由绑定)。 + /// + public long TenantId { get; init; } + + /// + /// 角色 ID(由路由绑定)。 + /// + public long RoleId { get; init; } + + /// + /// 角色名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 角色描述。 + /// + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRolePermissionsCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRolePermissionsCommand.cs new file mode 100644 index 0000000..7cb0287 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateTenantRolePermissionsCommand.cs @@ -0,0 +1,24 @@ +using MediatR; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 更新租户角色权限命令。 +/// +public sealed record UpdateTenantRolePermissionsCommand : IRequest +{ + /// + /// 租户 ID(由路由绑定)。 + /// + public long TenantId { get; init; } + + /// + /// 角色 ID(由路由绑定)。 + /// + public long RoleId { get; init; } + + /// + /// 权限 ID 集合(替换模式)。 + /// + public IReadOnlyCollection PermissionIds { get; init; } = []; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateTenantRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateTenantRoleCommandHandler.cs new file mode 100644 index 0000000..160c29e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateTenantRoleCommandHandler.cs @@ -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; + +/// +/// 创建租户角色命令处理器。 +/// +public sealed class CreateTenantRoleCommandHandler(IRoleRepository roleRepository) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetTenantRolePermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetTenantRolePermissionsQueryHandler.cs new file mode 100644 index 0000000..4a6d1ef --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetTenantRolePermissionsQueryHandler.cs @@ -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; + +/// +/// 获取租户角色权限列表查询处理器。 +/// +public sealed class GetTenantRolePermissionsQueryHandler( + IRoleRepository roleRepository, + IRolePermissionRepository rolePermissionRepository, + IPermissionRepository permissionRepository) + : IRequestHandler> +{ + /// + public async Task> 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(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRoleCommandHandler.cs new file mode 100644 index 0000000..0f6798c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRoleCommandHandler.cs @@ -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; + +/// +/// 更新租户角色命令处理器。 +/// +public sealed class UpdateTenantRoleCommandHandler(IRoleRepository roleRepository) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRolePermissionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRolePermissionsCommandHandler.cs new file mode 100644 index 0000000..920a7d6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateTenantRolePermissionsCommandHandler.cs @@ -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; + +/// +/// 更新租户角色权限命令处理器。 +/// +public sealed class UpdateTenantRolePermissionsCommandHandler( + IRoleRepository roleRepository, + IRolePermissionRepository rolePermissionRepository) + : IRequestHandler +{ + /// + public async Task 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; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/GetTenantRolePermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/GetTenantRolePermissionsQuery.cs new file mode 100644 index 0000000..521822b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/GetTenantRolePermissionsQuery.cs @@ -0,0 +1,20 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Queries; + +/// +/// 获取租户角色权限列表查询。 +/// +public sealed record GetTenantRolePermissionsQuery : IRequest> +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; init; } + + /// + /// 角色 ID。 + /// + public long RoleId { get; init; } +}