feat: 角色模板改为数据库管理支持前端自定义

This commit is contained in:
2025-12-03 20:38:26 +08:00
parent 19137f3cf7
commit 6a84141799
28 changed files with 901 additions and 652 deletions

View File

@@ -55,6 +55,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<IPermissionRepository, EfPermissionRepository>();
services.AddScoped<IUserRoleRepository, EfUserRoleRepository>();
services.AddScoped<IRolePermissionRepository, EfRolePermissionRepository>();
services.AddScoped<IRoleTemplateRepository, EfRoleTemplateRepository>();
services.AddScoped<IJwtTokenService, JwtTokenService>();
services.AddScoped<IRefreshTokenStore, RedisRefreshTokenStore>();
services.AddScoped<ILoginRateLimiter, RedisLoginRateLimiter>();

View File

@@ -11,6 +11,11 @@ public sealed class AdminSeedOptions
/// 初始用户列表。
/// </summary>
public List<SeedUserOptions> Users { get; set; } = new();
/// <summary>
/// 角色模板种子列表。
/// </summary>
public List<RoleTemplateSeedOptions> RoleTemplates { get; set; } = new();
}
/// <summary>
@@ -56,3 +61,36 @@ public sealed class SeedUserOptions
/// </summary>
public string[] Permissions { get; set; } = Array.Empty<string>();
}
/// <summary>
/// 角色模板种子配置。
/// </summary>
public sealed class RoleTemplateSeedOptions
{
/// <summary>
/// 模板编码。
/// </summary>
[Required]
public string TemplateCode { get; set; } = string.Empty;
/// <summary>
/// 模板名称。
/// </summary>
[Required]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 模板描述。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 权限编码集合。
/// </summary>
public string[] Permissions { get; set; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,117 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Identity.Entities;
using TakeoutSaaS.Domain.Identity.Repositories;
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// <summary>
/// 角色模板仓储实现。
/// </summary>
public sealed class EfRoleTemplateRepository(IdentityDbContext dbContext) : IRoleTemplateRepository
{
public Task<IReadOnlyList<RoleTemplate>> GetAllAsync(bool? isActive, CancellationToken cancellationToken = default)
{
var query = dbContext.RoleTemplates.AsNoTracking();
if (isActive.HasValue)
{
query = query.Where(x => x.IsActive == isActive.Value);
}
return query
.OrderBy(x => x.TemplateCode)
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<RoleTemplate>)t.Result, cancellationToken);
}
public Task<RoleTemplate?> FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default)
{
var normalized = templateCode.Trim();
return dbContext.RoleTemplates.AsNoTracking().FirstOrDefaultAsync(x => x.TemplateCode == normalized, cancellationToken);
}
public Task<IReadOnlyList<RoleTemplatePermission>> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default)
{
return dbContext.RoleTemplatePermissions.AsNoTracking()
.Where(x => x.RoleTemplateId == roleTemplateId)
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<RoleTemplatePermission>)t.Result, cancellationToken);
}
public async Task<IDictionary<long, IReadOnlyList<RoleTemplatePermission>>> GetPermissionsAsync(IEnumerable<long> roleTemplateIds, CancellationToken cancellationToken = default)
{
var ids = roleTemplateIds.Distinct().ToArray();
if (ids.Length == 0)
{
return new Dictionary<long, IReadOnlyList<RoleTemplatePermission>>();
}
var permissions = await dbContext.RoleTemplatePermissions.AsNoTracking()
.Where(x => ids.Contains(x.RoleTemplateId))
.ToListAsync(cancellationToken);
return permissions
.GroupBy(x => x.RoleTemplateId)
.ToDictionary(g => g.Key, g => (IReadOnlyList<RoleTemplatePermission>)g.ToList());
}
public async Task AddAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default)
{
template.TemplateCode = template.TemplateCode.Trim();
template.Name = template.Name.Trim();
await dbContext.RoleTemplates.AddAsync(template, cancellationToken);
await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken);
}
public async Task UpdateAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default)
{
template.TemplateCode = template.TemplateCode.Trim();
template.Name = template.Name.Trim();
dbContext.RoleTemplates.Update(template);
await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken);
}
public async Task DeleteAsync(long roleTemplateId, CancellationToken cancellationToken = default)
{
var entity = await dbContext.RoleTemplates.FirstOrDefaultAsync(x => x.Id == roleTemplateId, cancellationToken);
if (entity != null)
{
var permissions = dbContext.RoleTemplatePermissions.Where(x => x.RoleTemplateId == roleTemplateId);
dbContext.RoleTemplatePermissions.RemoveRange(permissions);
dbContext.RoleTemplates.Remove(entity);
}
}
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken);
private async Task ReplacePermissionsInternalAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken)
{
// 确保模板已持久化,便于 FK 正确填充
if (!dbContext.Entry(template).IsKeySet || template.Id == 0)
{
await dbContext.SaveChangesAsync(cancellationToken);
}
var normalized = permissionCodes
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var existing = await dbContext.RoleTemplatePermissions
.Where(x => x.RoleTemplateId == template.Id)
.ToListAsync(cancellationToken);
dbContext.RoleTemplatePermissions.RemoveRange(existing);
var toAdd = normalized.Select(code => new RoleTemplatePermission
{
RoleTemplateId = template.Id,
PermissionCode = code
});
await dbContext.RoleTemplatePermissions.AddRangeAsync(toAdd, cancellationToken);
}
}

View File

@@ -12,6 +12,8 @@ using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser;
using DomainPermission = TakeoutSaaS.Domain.Identity.Entities.Permission;
using DomainRole = TakeoutSaaS.Domain.Identity.Entities.Role;
using DomainRolePermission = TakeoutSaaS.Domain.Identity.Entities.RolePermission;
using DomainRoleTemplate = TakeoutSaaS.Domain.Identity.Entities.RoleTemplate;
using DomainRoleTemplatePermission = TakeoutSaaS.Domain.Identity.Entities.RoleTemplatePermission;
using DomainUserRole = TakeoutSaaS.Domain.Identity.Entities.UserRole;
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
@@ -37,6 +39,8 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
return;
}
await SeedRoleTemplatesAsync(context, options.RoleTemplates, cancellationToken);
foreach (var userOptions in options.Users)
{
using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId);
@@ -159,6 +163,66 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private static async Task SeedRoleTemplatesAsync(
IdentityDbContext context,
IList<RoleTemplateSeedOptions> templates,
CancellationToken cancellationToken)
{
if (templates is null || templates.Count == 0)
{
return;
}
foreach (var templateOptions in templates)
{
if (string.IsNullOrWhiteSpace(templateOptions.TemplateCode) || string.IsNullOrWhiteSpace(templateOptions.Name))
{
continue;
}
var code = templateOptions.TemplateCode.Trim();
var existing = await context.RoleTemplates.FirstOrDefaultAsync(x => x.TemplateCode == code, cancellationToken);
if (existing == null)
{
existing = new DomainRoleTemplate
{
TemplateCode = code,
Name = templateOptions.Name.Trim(),
Description = templateOptions.Description,
IsActive = templateOptions.IsActive
};
await context.RoleTemplates.AddAsync(existing, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
else
{
existing.Name = templateOptions.Name.Trim();
existing.Description = templateOptions.Description;
existing.IsActive = templateOptions.IsActive;
context.RoleTemplates.Update(existing);
await context.SaveChangesAsync(cancellationToken);
}
var permissionCodes = NormalizeValues(templateOptions.Permissions);
var existingPermissions = await context.RoleTemplatePermissions
.Where(x => x.RoleTemplateId == existing.Id)
.ToListAsync(cancellationToken);
context.RoleTemplatePermissions.RemoveRange(existingPermissions);
var toAdd = permissionCodes.Select(code => new DomainRoleTemplatePermission
{
RoleTemplateId = existing.Id,
PermissionCode = code
});
await context.RoleTemplatePermissions.AddRangeAsync(toAdd, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
}
private static string[] NormalizeValues(string[]? values)
=> values == null
? []

View File

@@ -34,6 +34,16 @@ public sealed class IdentityDbContext(
/// </summary>
public DbSet<Role> Roles => Set<Role>();
/// <summary>
/// 角色模板集合(平台级)。
/// </summary>
public DbSet<RoleTemplate> RoleTemplates => Set<RoleTemplate>();
/// <summary>
/// 角色模板权限集合。
/// </summary>
public DbSet<RoleTemplatePermission> RoleTemplatePermissions => Set<RoleTemplatePermission>();
/// <summary>
/// 权限集合。
/// </summary>
@@ -59,6 +69,8 @@ public sealed class IdentityDbContext(
ConfigureIdentityUser(modelBuilder.Entity<IdentityUser>());
ConfigureMiniUser(modelBuilder.Entity<MiniUser>());
ConfigureRole(modelBuilder.Entity<Role>());
ConfigureRoleTemplate(modelBuilder.Entity<RoleTemplate>());
ConfigureRoleTemplatePermission(modelBuilder.Entity<RoleTemplatePermission>());
ConfigurePermission(modelBuilder.Entity<Permission>());
ConfigureUserRole(modelBuilder.Entity<UserRole>());
ConfigureRolePermission(modelBuilder.Entity<RolePermission>());
@@ -133,6 +145,28 @@ public sealed class IdentityDbContext(
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
}
private static void ConfigureRoleTemplate(EntityTypeBuilder<RoleTemplate> builder)
{
builder.ToTable("role_templates");
builder.HasKey(x => x.Id);
builder.Property(x => x.TemplateCode).HasMaxLength(64).IsRequired();
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.Description).HasMaxLength(256);
builder.Property(x => x.IsActive).IsRequired();
ConfigureAuditableEntity(builder);
builder.HasIndex(x => x.TemplateCode).IsUnique();
}
private static void ConfigureRoleTemplatePermission(EntityTypeBuilder<RoleTemplatePermission> builder)
{
builder.ToTable("role_template_permissions");
builder.HasKey(x => x.Id);
builder.Property(x => x.RoleTemplateId).IsRequired();
builder.Property(x => x.PermissionCode).HasMaxLength(128).IsRequired();
ConfigureAuditableEntity(builder);
builder.HasIndex(x => new { x.RoleTemplateId, x.PermissionCode }).IsUnique();
}
private static void ConfigureUserRole(EntityTypeBuilder<UserRole> builder)
{
builder.ToTable("user_roles");