feat: 新增RBAC角色模板复制与初始化

This commit is contained in:
2025-12-03 19:55:25 +08:00
parent 0c329669a9
commit ea33e6fefe
23 changed files with 1054 additions and 2 deletions

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.Identity.Abstractions;
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;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 角色模板复制处理器。
/// </summary>
public sealed class CopyRoleTemplateCommandHandler(
IRoleTemplateProvider roleTemplateProvider,
IRoleRepository roleRepository,
IPermissionRepository permissionRepository,
IRolePermissionRepository rolePermissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<CopyRoleTemplateCommand, RoleDto>
{
/// <inheritdoc />
public async Task<RoleDto> Handle(CopyRoleTemplateCommand request, CancellationToken cancellationToken)
{
var template = roleTemplateProvider.FindByCode(request.TemplateCode)
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.TemplateCode} 不存在");
var tenantId = tenantProvider.GetCurrentTenantId();
var roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim();
var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim();
var roleDescription = request.Description ?? template.Description;
// 1. 准备或更新角色主体(幂等创建)。
var role = await roleRepository.FindByCodeAsync(roleCode, tenantId, cancellationToken);
if (role is null)
{
role = new Role
{
TenantId = tenantId,
Name = roleName,
Code = roleCode,
Description = roleDescription
};
await roleRepository.AddAsync(role, cancellationToken);
}
else
{
if (!string.IsNullOrWhiteSpace(request.RoleName))
{
role.Name = roleName;
}
if (request.Description is not null)
{
role.Description = roleDescription;
}
await roleRepository.UpdateAsync(role, cancellationToken);
}
// 2. 确保模板权限全部存在,不存在则按模板定义创建。
var targetPermissionCodes = template.Permissions
.Select(permission => permission.Code)
.Where(code => !string.IsNullOrWhiteSpace(code))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, targetPermissionCodes, cancellationToken);
var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
foreach (var permissionDefinition in template.Permissions)
{
if (permissionMap.ContainsKey(permissionDefinition.Code))
{
continue;
}
var permission = new Permission
{
TenantId = tenantId,
Name = permissionDefinition.Name,
Code = permissionDefinition.Code,
Description = permissionDefinition.Description
};
await permissionRepository.AddAsync(permission, cancellationToken);
permissionMap[permissionDefinition.Code] = permission;
}
await roleRepository.SaveChangesAsync(cancellationToken);
// 3. 绑定缺失的权限,保留租户自定义的已有授权。
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken);
var existingPermissionIds = rolePermissions
.Select(x => x.PermissionId)
.ToHashSet();
var targetPermissionIds = targetPermissionCodes
.Select(code => permissionMap[code].Id)
.ToHashSet();
var toAdd = targetPermissionIds.Except(existingPermissionIds).ToArray();
if (toAdd.Length > 0)
{
var relations = toAdd.Select(permissionId => new RolePermission
{
TenantId = tenantId,
RoleId = role.Id,
PermissionId = permissionId
});
await rolePermissionRepository.AddRangeAsync(relations, cancellationToken);
}
await rolePermissionRepository.SaveChangesAsync(cancellationToken);
return new RoleDto
{
Id = role.Id,
TenantId = role.TenantId,
Name = role.Name,
Code = role.Code,
Description = role.Description
};
}
}

View File

@@ -0,0 +1,20 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 角色模板详情查询处理器。
/// </summary>
public sealed class GetRoleTemplateQueryHandler(IRoleTemplateProvider roleTemplateProvider)
: IRequestHandler<GetRoleTemplateQuery, RoleTemplateDto?>
{
/// <inheritdoc />
public Task<RoleTemplateDto?> Handle(GetRoleTemplateQuery request, CancellationToken cancellationToken)
{
var template = roleTemplateProvider.FindByCode(request.TemplateCode);
return Task.FromResult(template is null ? null : TemplateMapper.ToDto(template));
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 租户角色模板批量初始化处理器。
/// </summary>
public sealed class InitializeRoleTemplatesCommandHandler(
IRoleTemplateProvider roleTemplateProvider,
IMediator mediator)
: IRequestHandler<InitializeRoleTemplatesCommand, IReadOnlyList<RoleDto>>
{
/// <inheritdoc />
public async Task<IReadOnlyList<RoleDto>> Handle(InitializeRoleTemplatesCommand request, CancellationToken cancellationToken)
{
// 1. 解析需要初始化的模板编码,默认取全部预置模板。
var requestedCodes = request.TemplateCodes?
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var targetCodes = requestedCodes?.Length > 0
? requestedCodes
: roleTemplateProvider.GetTemplates().Select(template => template.TemplateCode).ToArray();
if (targetCodes.Length == 0)
{
return Array.Empty<RoleDto>();
}
// 2. 逐个复制模板,幂等写入角色与权限。
var roles = new List<RoleDto>(targetCodes.Length);
foreach (var templateCode in targetCodes)
{
var template = roleTemplateProvider.FindByCode(templateCode)
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {templateCode} 不存在");
var role = await mediator.Send(new CopyRoleTemplateCommand
{
TemplateCode = template.TemplateCode
}, cancellationToken);
roles.Add(role);
}
return roles;
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 角色模板列表查询处理器。
/// </summary>
public sealed class ListRoleTemplatesQueryHandler(IRoleTemplateProvider roleTemplateProvider)
: IRequestHandler<ListRoleTemplatesQuery, IReadOnlyList<RoleTemplateDto>>
{
/// <inheritdoc />
public Task<IReadOnlyList<RoleTemplateDto>> Handle(ListRoleTemplatesQuery request, CancellationToken cancellationToken)
{
var templates = roleTemplateProvider.GetTemplates()
.OrderBy(template => template.TemplateCode, StringComparer.OrdinalIgnoreCase)
.Select(TemplateMapper.ToDto)
.ToArray();
return Task.FromResult<IReadOnlyList<RoleTemplateDto>>(templates);
}
}

View File

@@ -0,0 +1,37 @@
using System.Linq;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Templates;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 模板 DTO 映射工具。
/// </summary>
internal static class TemplateMapper
{
/// <summary>
/// 将角色模板定义映射为 DTO。
/// </summary>
/// <param name="definition">角色模板定义。</param>
/// <returns>模板 DTO。</returns>
public static RoleTemplateDto ToDto(RoleTemplateDefinition definition)
{
return new RoleTemplateDto
{
TemplateCode = definition.TemplateCode,
Name = definition.Name,
Description = definition.Description,
Permissions = definition.Permissions.Select(ToDto).ToArray()
};
}
private static PermissionTemplateDto ToDto(PermissionTemplateDefinition definition)
{
return new PermissionTemplateDto
{
Code = definition.Code,
Name = definition.Name,
Description = definition.Description
};
}
}