feat: add role template and tenant role apis

This commit is contained in:
2025-12-05 20:55:56 +08:00
parent 373c97e965
commit 3a38ade302
12 changed files with 8799 additions and 2 deletions

View File

@@ -0,0 +1,217 @@
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;
/// <summary>
/// 角色模板管理(平台蓝本)。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/admin/v{version:apiVersion}/role-templates")]
public sealed class RoleTemplatesController(IMediator mediator) : BaseApiController
{
/// <summary>
/// 分页查询角色模板。
/// </summary>
/// <param name="isActive">是否启用筛选。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色模板列表。</returns>
[HttpGet]
[PermissionAuthorize("identity:role:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleTemplateDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<RoleTemplateDto>>> List([FromQuery] bool? isActive, CancellationToken cancellationToken)
{
// 1. 构造查询参数
var query = new ListRoleTemplatesQuery { IsActive = isActive };
// 2. 查询模板集合
var result = await mediator.Send(query, cancellationToken);
// 3. 返回模板列表
return ApiResponse<IReadOnlyList<RoleTemplateDto>>.Ok(result);
}
/// <summary>
/// 克隆角色模板。
/// </summary>
/// <param name="templateCode">源模板编码。</param>
/// <param name="command">克隆命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>新模板详情。</returns>
[HttpPost("{templateCode}/clone")]
[PermissionAuthorize("role-template:create")]
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<RoleTemplateDto>> Clone(
string templateCode,
[FromBody, Required] CloneRoleTemplateCommand command,
CancellationToken cancellationToken)
{
// 1. 绑定源模板编码
command = command with { SourceTemplateCode = templateCode };
// 2. 执行克隆
var result = await mediator.Send(command, cancellationToken);
// 3. 返回新模板
return ApiResponse<RoleTemplateDto>.Ok(result);
}
/// <summary>
/// 获取角色模板详情。
/// </summary>
/// <param name="templateCode">模板编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色模板详情。</returns>
[HttpGet("{templateCode}")]
[PermissionAuthorize("identity:role:read")]
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<RoleTemplateDto>> Detail(string templateCode, CancellationToken cancellationToken)
{
// 1. 查询模板详情
var result = await mediator.Send(new GetRoleTemplateQuery { TemplateCode = templateCode }, cancellationToken);
// 2. 返回模板或 404
return result is null
? ApiResponse<RoleTemplateDto>.Error(StatusCodes.Status404NotFound, "角色模板不存在")
: ApiResponse<RoleTemplateDto>.Ok(result);
}
/// <summary>
/// 创建角色模板。
/// </summary>
/// <param name="command">创建命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>创建后的模板。</returns>
[HttpPost]
[PermissionAuthorize("role-template:create")]
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<RoleTemplateDto>> Create([FromBody, Required] CreateRoleTemplateCommand command, CancellationToken cancellationToken)
{
// 1. 创建模板
var result = await mediator.Send(command, cancellationToken);
// 2. 返回创建结果
return ApiResponse<RoleTemplateDto>.Ok(result);
}
/// <summary>
/// 获取模板的权限列表。
/// </summary>
/// <param name="templateCode">模板编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>权限集合。</returns>
[HttpGet("{templateCode}/permissions")]
[PermissionAuthorize("identity:role:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionTemplateDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<PermissionTemplateDto>>> GetPermissions(string templateCode, CancellationToken cancellationToken)
{
// 1. 查询模板权限
var result = await mediator.Send(new RoleTemplatePermissionsQuery { TemplateCode = templateCode }, cancellationToken);
// 2. 返回权限集合
return ApiResponse<IReadOnlyList<PermissionTemplateDto>>.Ok(result);
}
/// <summary>
/// 更新角色模板。
/// </summary>
/// <param name="templateCode">模板编码。</param>
/// <param name="command">更新命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>更新后的模板。</returns>
[HttpPut("{templateCode}")]
[PermissionAuthorize("role-template:update")]
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<RoleTemplateDto>> Update(
string templateCode,
[FromBody, Required] UpdateRoleTemplateCommand command,
CancellationToken cancellationToken)
{
// 1. 绑定模板编码
command = command with { TemplateCode = templateCode };
// 2. 执行更新
var result = await mediator.Send(command, cancellationToken);
// 3. 返回更新结果或 404
return result is null
? ApiResponse<RoleTemplateDto>.Error(StatusCodes.Status404NotFound, "角色模板不存在")
: ApiResponse<RoleTemplateDto>.Ok(result);
}
/// <summary>
/// 删除角色模板。
/// </summary>
/// <param name="templateCode">模板编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>删除结果。</returns>
[HttpDelete("{templateCode}")]
[PermissionAuthorize("role-template:delete")]
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
public async Task<ApiResponse<bool>> Delete(string templateCode, CancellationToken cancellationToken)
{
// 1. 执行删除
var result = await mediator.Send(new DeleteRoleTemplateCommand { TemplateCode = templateCode }, cancellationToken);
// 2. 返回执行结果
return ApiResponse<bool>.Ok(result);
}
/// <summary>
/// 为当前租户批量初始化预置角色模板。
/// </summary>
/// <param name="command">初始化命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>生成的租户角色列表。</returns>
[HttpPost("init")]
[PermissionAuthorize("identity:role:create")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<RoleDto>>> Initialize(
[FromBody] InitializeRoleTemplatesCommand? command,
CancellationToken cancellationToken)
{
// 1. 确保命令存在
command ??= new InitializeRoleTemplatesCommand();
// 2. 初始化模板到租户
var result = await mediator.Send(command, cancellationToken);
// 3. 返回新建的角色列表
return ApiResponse<IReadOnlyList<RoleDto>>.Ok(result);
}
/// <summary>
/// 将单个模板初始化到当前租户。
/// </summary>
/// <param name="templateCode">模板编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>生成的角色列表。</returns>
[HttpPost("{templateCode}/initialize-tenant")]
[PermissionAuthorize("identity:role:create")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<RoleDto>>> InitializeSingle(string templateCode, CancellationToken cancellationToken)
{
// 1. 构造初始化命令
var command = new InitializeRoleTemplatesCommand
{
TemplateCodes = new[] { templateCode }
};
// 2. 初始化模板到租户
var result = await mediator.Send(command, cancellationToken);
// 3. 返回生成的角色列表
return ApiResponse<IReadOnlyList<RoleDto>>.Ok(result);
}
}

View File

@@ -0,0 +1,209 @@
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.Abstractions.Tenancy;
using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers;
/// <summary>
/// 租户角色管理(实例层)。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/roles")]
public sealed class TenantRolesController(IMediator mediator, ITenantProvider tenantProvider) : BaseApiController
{
/// <summary>
/// 租户角色分页。
/// </summary>
[HttpGet]
[PermissionAuthorize("identity:role:read")]
[ProducesResponseType(typeof(ApiResponse<PagedResult<RoleDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<RoleDto>>> List(
long tenantId,
[FromQuery] SearchRolesQuery query,
CancellationToken cancellationToken)
{
// 1. 校验路由租户与上下文一致
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != currentTenantId)
{
return ApiResponse<PagedResult<RoleDto>>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 查询角色分页
var result = await mediator.Send(query, cancellationToken);
// 3. 返回分页数据
return ApiResponse<PagedResult<RoleDto>>.Ok(result);
}
/// <summary>
/// 角色详情(含权限)。
/// </summary>
[HttpGet("{roleId:long}")]
[PermissionAuthorize("identity:role:read")]
[ProducesResponseType(typeof(ApiResponse<RoleDetailDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<RoleDetailDto>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<RoleDetailDto>> Detail(long tenantId, long roleId, CancellationToken cancellationToken)
{
// 1. 校验租户上下文
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != currentTenantId)
{
return ApiResponse<RoleDetailDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 查询角色详情
var result = await mediator.Send(new RoleDetailQuery { RoleId = roleId }, cancellationToken);
// 3. 返回数据或 404
return result is null
? ApiResponse<RoleDetailDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
: ApiResponse<RoleDetailDto>.Ok(result);
}
/// <summary>
/// 创建角色。
/// </summary>
[HttpPost]
[PermissionAuthorize("identity:role:create")]
[ProducesResponseType(typeof(ApiResponse<RoleDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<RoleDto>> Create(
long tenantId,
[FromBody, Required] CreateRoleCommand command,
CancellationToken cancellationToken)
{
// 1. 校验租户上下文
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != currentTenantId)
{
return ApiResponse<RoleDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 创建角色
var result = await mediator.Send(command, cancellationToken);
// 3. 返回创建结果
return ApiResponse<RoleDto>.Ok(result);
}
/// <summary>
/// 更新角色。
/// </summary>
[HttpPut("{roleId:long}")]
[PermissionAuthorize("identity:role:update")]
[ProducesResponseType(typeof(ApiResponse<RoleDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<RoleDto>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<RoleDto>> Update(
long tenantId,
long roleId,
[FromBody, Required] UpdateRoleCommand command,
CancellationToken cancellationToken)
{
// 1. 校验租户上下文
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != currentTenantId)
{
return ApiResponse<RoleDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 绑定角色 ID
command = command with { RoleId = roleId };
// 3. 执行更新
var result = await mediator.Send(command, cancellationToken);
// 4. 返回结果或 404
return result is null
? ApiResponse<RoleDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
: ApiResponse<RoleDto>.Ok(result);
}
/// <summary>
/// 删除角色。
/// </summary>
[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 currentTenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != currentTenantId)
{
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 执行删除
var command = new DeleteRoleCommand { RoleId = roleId };
var result = await mediator.Send(command, cancellationToken);
// 3. 返回结果
return ApiResponse<bool>.Ok(result);
}
/// <summary>
/// 获取角色权限列表。
/// </summary>
[HttpGet("{roleId:long}/permissions")]
[PermissionAuthorize("identity:role:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionDto>>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionDto>>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<IReadOnlyList<PermissionDto>>> GetPermissions(
long tenantId,
long roleId,
CancellationToken cancellationToken)
{
// 1. 校验租户上下文
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != currentTenantId)
{
return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 查询角色详情并提取权限
var detail = await mediator.Send(new RoleDetailQuery { RoleId = roleId }, cancellationToken);
if (detail is null)
{
return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status404NotFound, "角色不存在");
}
// 3. 返回权限集合
return ApiResponse<IReadOnlyList<PermissionDto>>.Ok(detail.Permissions);
}
/// <summary>
/// 覆盖角色权限。
/// </summary>
[HttpPut("{roleId:long}/permissions")]
[PermissionAuthorize("identity:role:bind-permission")]
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
public async Task<ApiResponse<bool>> BindPermissions(
long tenantId,
long roleId,
[FromBody, Required] BindRolePermissionsCommand command,
CancellationToken cancellationToken)
{
// 1. 校验租户上下文
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != currentTenantId)
{
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
}
// 2. 绑定角色 ID
command = command with { RoleId = roleId };
// 3. 覆盖授权
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<bool>.Ok(result);
}
}