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,40 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 克隆角色模板。
/// </summary>
public sealed record CloneRoleTemplateCommand : IRequest<RoleTemplateDto>
{
/// <summary>
/// 源模板编码。
/// </summary>
public string SourceTemplateCode { get; init; } = string.Empty;
/// <summary>
/// 新模板编码。
/// </summary>
public string NewTemplateCode { get; init; } = string.Empty;
/// <summary>
/// 新模板名称(为空则沿用源模板)。
/// </summary>
public string? Name { get; init; }
/// <summary>
/// 新模板描述(为空则沿用源模板)。
/// </summary>
public string? Description { get; init; }
/// <summary>
/// 是否启用(为空则沿用源模板)。
/// </summary>
public bool? IsActive { get; init; }
/// <summary>
/// 权限编码集合(为空则沿用源模板权限)。
/// </summary>
public IReadOnlyCollection<string>? PermissionCodes { get; init; }
}

View File

@@ -0,0 +1,37 @@
namespace TakeoutSaaS.Application.Identity.Contracts;
/// <summary>
/// 角色详情 DTO。
/// </summary>
public sealed record RoleDetailDto
{
/// <summary>
/// 角色 ID。
/// </summary>
public long Id { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
public long TenantId { get; init; }
/// <summary>
/// 角色名称。
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// 角色编码。
/// </summary>
public string Code { get; init; } = string.Empty;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; init; }
/// <summary>
/// 权限列表。
/// </summary>
public IReadOnlyList<PermissionDto> Permissions { get; init; } = [];
}

View File

@@ -0,0 +1,73 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Domain.Identity.Entities;
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 CloneRoleTemplateCommandHandler(IRoleTemplateRepository roleTemplateRepository)
: IRequestHandler<CloneRoleTemplateCommand, RoleTemplateDto>
{
/// <inheritdoc />
public async Task<RoleTemplateDto> Handle(CloneRoleTemplateCommand request, CancellationToken cancellationToken)
{
// 1. 校验源模板是否存在
var source = await roleTemplateRepository.FindByCodeAsync(request.SourceTemplateCode, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.SourceTemplateCode} 不存在");
// 2. 校验新模板编码是否冲突
var exists = await roleTemplateRepository.FindByCodeAsync(request.NewTemplateCode, cancellationToken);
if (exists is not null)
{
throw new BusinessException(ErrorCodes.Conflict, $"角色模板编码 {request.NewTemplateCode} 已存在");
}
// 3. 获取源模板权限
var sourcePermissions = await roleTemplateRepository.GetPermissionsAsync(source.Id, cancellationToken);
var permissionCodes = request.PermissionCodes is not null && request.PermissionCodes.Count > 0
? request.PermissionCodes.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
: sourcePermissions
.Select(x => x.PermissionCode)
.Where(code => !string.IsNullOrWhiteSpace(code))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
// 4. 构造新模板实体
var target = new RoleTemplate
{
TemplateCode = request.NewTemplateCode.Trim(),
Name = string.IsNullOrWhiteSpace(request.Name) ? source.Name : request.Name.Trim(),
Description = request.Description ?? source.Description,
IsActive = request.IsActive ?? source.IsActive
};
// 5. 持久化新模板与权限
await roleTemplateRepository.AddAsync(target, permissionCodes, cancellationToken);
await roleTemplateRepository.SaveChangesAsync(cancellationToken);
// 6. 映射返回 DTO
var permissionDtos = permissionCodes
.Select(code => new PermissionTemplateDto
{
Code = code,
Name = code,
Description = code
})
.ToList();
return new RoleTemplateDto
{
TemplateCode = target.TemplateCode,
Name = target.Name,
Description = target.Description,
IsActive = target.IsActive,
Permissions = permissionDtos
};
}
}

View File

@@ -0,0 +1,60 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 角色详情查询处理器。
/// </summary>
public sealed class RoleDetailQueryHandler(
IRoleRepository roleRepository,
IRolePermissionRepository rolePermissionRepository,
IPermissionRepository permissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<RoleDetailQuery, RoleDetailDto?>
{
/// <inheritdoc />
public async Task<RoleDetailDto?> Handle(RoleDetailQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询角色
var tenantId = tenantProvider.GetCurrentTenantId();
var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken);
if (role is null)
{
return null;
}
// 2. 查询角色权限关系
var relations = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken);
var permissionIds = relations.Select(x => x.PermissionId).ToArray();
// 3. 拉取权限实体
var permissions = permissionIds.Length == 0
? Array.Empty<Domain.Identity.Entities.Permission>()
: await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
// 4. 映射 DTO
var permissionDtos = permissions
.Select(x => new PermissionDto
{
Id = x.Id,
Code = x.Code,
Name = x.Name,
Description = x.Description
})
.ToList();
return new RoleDetailDto
{
Id = role.Id,
TenantId = role.TenantId,
Name = role.Name,
Code = role.Code,
Description = role.Description,
Permissions = permissionDtos
};
}
}

View File

@@ -0,0 +1,40 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
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 RoleTemplatePermissionsQueryHandler(IRoleTemplateRepository roleTemplateRepository)
: IRequestHandler<RoleTemplatePermissionsQuery, IReadOnlyList<PermissionTemplateDto>>
{
/// <inheritdoc />
public async Task<IReadOnlyList<PermissionTemplateDto>> Handle(RoleTemplatePermissionsQuery request, CancellationToken cancellationToken)
{
// 1. 校验模板存在
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.TemplateCode} 不存在");
// 2. 查询模板权限
var permissions = await roleTemplateRepository.GetPermissionsAsync(template.Id, cancellationToken);
// 3. 映射 DTO
var dto = permissions
.Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode))
.Select(x => new PermissionTemplateDto
{
Code = x.PermissionCode,
Name = x.PermissionCode,
Description = x.PermissionCode
})
.ToList();
// 4. 返回权限列表
return dto;
}
}

View File

@@ -0,0 +1,15 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Queries;
/// <summary>
/// 查询角色详情(含权限)。
/// </summary>
public sealed class RoleDetailQuery : IRequest<RoleDetailDto?>
{
/// <summary>
/// 角色 ID。
/// </summary>
public long RoleId { get; init; }
}

View File

@@ -0,0 +1,15 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Queries;
/// <summary>
/// 查询角色模板权限列表。
/// </summary>
public sealed class RoleTemplatePermissionsQuery : IRequest<IReadOnlyList<PermissionTemplateDto>>
{
/// <summary>
/// 模板编码。
/// </summary>
public string TemplateCode { get; init; } = string.Empty;
}