feat: 角色模板改为数据库管理支持前端自定义
This commit is contained in:
@@ -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>();
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
? []
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user