feat: 实现租户角色列表查询和删除接口

- 获取租户角色列表:GET /api/admin/v1/tenants/{tenantId}/roles
- 删除租户角色:DELETE /api/admin/v1/tenants/{tenantId}/roles/{roleId}

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 07:53:25 +00:00
parent 2dbd004ce0
commit 0bcad8ba7e
5 changed files with 191 additions and 0 deletions

View File

@@ -19,6 +19,41 @@ namespace TakeoutSaaS.AdminApi.Controllers;
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/roles")] [Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/roles")]
public sealed class TenantRolesController(IMediator mediator) : BaseApiController public sealed class TenantRolesController(IMediator mediator) : BaseApiController
{ {
/// <summary>
/// 获取租户角色列表。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="keyword">关键字(角色名称/编码)。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页条数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色分页列表。</returns>
[HttpGet]
[PermissionAuthorize("identity:role:read")]
[ProducesResponseType(typeof(ApiResponse<PagedResult<RoleDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<RoleDto>>> List(
long tenantId,
[FromQuery] string? keyword,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken cancellationToken = default)
{
// 1. 构造查询
var query = new ListTenantRolesQuery
{
TenantId = tenantId,
Keyword = keyword,
Page = page,
PageSize = pageSize
};
// 2. 执行查询
var result = await mediator.Send(query, cancellationToken);
// 3. 返回分页结果
return ApiResponse<PagedResult<RoleDto>>.Ok(result);
}
/// <summary> /// <summary>
/// 创建租户角色。 /// 创建租户角色。
/// </summary> /// </summary>
@@ -125,4 +160,29 @@ 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="cancellationToken">取消标记。</param>
/// <returns>删除结果。</returns>
[HttpDelete("{roleId:long}")]
[PermissionAuthorize("identity:role:delete")]
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
public async Task<ApiResponse<bool>> Delete(
long tenantId,
long roleId,
CancellationToken cancellationToken)
{
// 1. 构造命令
var command = new DeleteTenantRoleCommand { TenantId = tenantId, RoleId = roleId };
// 2. 执行删除
var result = await mediator.Send(command, cancellationToken);
// 3. 返回删除结果
return ApiResponse<bool>.Ok(result);
}
} }

View File

@@ -0,0 +1,19 @@
using MediatR;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 删除租户角色命令。
/// </summary>
public sealed record DeleteTenantRoleCommand : IRequest<bool>
{
/// <summary>
/// 租户 ID由路由绑定
/// </summary>
public long TenantId { get; init; }
/// <summary>
/// 角色 ID由路由绑定
/// </summary>
public long RoleId { get; init; }
}

View File

@@ -0,0 +1,33 @@
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;
/// <summary>
/// 删除租户角色命令处理器。
/// </summary>
public sealed class DeleteTenantRoleCommandHandler(IRoleRepository roleRepository)
: IRequestHandler<DeleteTenantRoleCommand, bool>
{
/// <inheritdoc />
public async Task<bool> Handle(DeleteTenantRoleCommand request, CancellationToken cancellationToken)
{
// 1. 校验角色存在
var role = await roleRepository.FindByIdAsync(PortalType.Tenant, request.TenantId, request.RoleId, cancellationToken);
if (role is null)
{
throw new BusinessException(ErrorCodes.NotFound, "角色不存在");
}
// 2. 执行软删除
await roleRepository.DeleteAsync(PortalType.Tenant, request.TenantId, request.RoleId, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
// 3. 返回成功
return true;
}
}

View File

@@ -0,0 +1,48 @@
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.Results;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 获取租户角色列表查询处理器。
/// </summary>
public sealed class ListTenantRolesQueryHandler(IRoleRepository roleRepository)
: IRequestHandler<ListTenantRolesQuery, PagedResult<RoleDto>>
{
/// <inheritdoc />
public async Task<PagedResult<RoleDto>> Handle(ListTenantRolesQuery request, CancellationToken cancellationToken)
{
// 1. 查询租户下所有角色
var roles = await roleRepository.SearchAsync(PortalType.Tenant, request.TenantId, request.Keyword, cancellationToken);
// 2. 计算分页参数
var totalCount = roles.Count;
var page = Math.Max(1, request.Page);
var pageSize = Math.Clamp(request.PageSize, 1, 100);
// 3. 应用分页
var pagedRoles = roles
.OrderBy(r => r.Code)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToArray();
// 4. 映射 DTO
var dtos = pagedRoles.Select(role => new RoleDto
{
Portal = role.Portal,
Id = role.Id,
TenantId = role.TenantId,
Code = role.Code,
Name = role.Name,
Description = role.Description
}).ToArray();
// 5. 返回分页结果
return new PagedResult<RoleDto>(dtos, page, pageSize, totalCount);
}
}

View File

@@ -0,0 +1,31 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.Identity.Queries;
/// <summary>
/// 获取租户角色列表查询。
/// </summary>
public sealed record ListTenantRolesQuery : IRequest<PagedResult<RoleDto>>
{
/// <summary>
/// 租户 ID。
/// </summary>
public long TenantId { get; init; }
/// <summary>
/// 关键字(角色名称/编码)。
/// </summary>
public string? Keyword { get; init; }
/// <summary>
/// 页码(从 1 开始)。
/// </summary>
public int Page { get; init; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; init; } = 20;
}