diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs index cdc727b..a5a2a42 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; @@ -11,10 +13,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// public sealed class GetUserPermissionsQueryHandler( IIdentityUserRepository identityUserRepository, + IUserRoleRepository userRoleRepository, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository, + IRolePermissionRepository rolePermissionRepository, ITenantProvider tenantProvider) : IRequestHandler { private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository; + private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; + private readonly IRoleRepository _roleRepository = roleRepository; + private readonly IPermissionRepository _permissionRepository = permissionRepository; + private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// @@ -27,6 +37,9 @@ public sealed class GetUserPermissionsQueryHandler( return null; } + var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); + var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + return new UserPermissionDto { UserId = user.Id, @@ -34,9 +47,42 @@ public sealed class GetUserPermissionsQueryHandler( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = roleCodes, + Permissions = permissionCodes, CreatedAt = user.CreatedAt }; } + + private async Task ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } + + private async Task ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + if (permissionIds.Length == 0) + { + return Array.Empty(); + } + + var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs index adfe198..07e3595 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using MediatR; using TakeoutSaaS.Application.Identity.Contracts; @@ -13,10 +15,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// public sealed class SearchUserPermissionsQueryHandler( IIdentityUserRepository identityUserRepository, + IUserRoleRepository userRoleRepository, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository, + IRolePermissionRepository rolePermissionRepository, ITenantProvider tenantProvider) : IRequestHandler> { private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository; + private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; + private readonly IRoleRepository _roleRepository = roleRepository; + private readonly IPermissionRepository _permissionRepository = permissionRepository; + private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// @@ -31,6 +41,7 @@ public sealed class SearchUserPermissionsQueryHandler( .Take(request.PageSize) .ToList(); + var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken); var items = paged.Select(user => new UserPermissionDto { UserId = user.Id, @@ -38,8 +49,8 @@ public sealed class SearchUserPermissionsQueryHandler( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = resolved[user.Id].roles, + Permissions = resolved[user.Id].permissions, CreatedAt = user.CreatedAt }).ToList(); @@ -64,4 +75,57 @@ public sealed class SearchUserPermissionsQueryHandler( : users.OrderBy(x => x.CreatedAt) }; } + + private async Task> ResolveRolesAndPermissionsAsync( + long tenantId, + IReadOnlyCollection users, + CancellationToken cancellationToken) + { + var userIds = users.Select(x => x.Id).ToArray(); + var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); + + var roles = roleIds.Length == 0 + ? Array.Empty() + : await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); + + var rolePermissions = roleIds.Length == 0 + ? Array.Empty() + : await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + + var permissions = permissionIds.Length == 0 + ? Array.Empty() + : await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); + + var rolePermissionsLookup = rolePermissions + .GroupBy(rp => rp.RoleId) + .ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer.Default); + + var result = new Dictionary(); + foreach (var userId in userIds) + { + var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray(); + var roleCodes = rolesForUser + .Select(rid => roleCodeMap.GetValueOrDefault(rid)) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Select(c => c!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var permissionCodes = rolesForUser + .SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty()) + .Select(pid => permissionCodeMap.GetValueOrDefault(pid)) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(code => code!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + result[userId] = (roleCodes, permissionCodes); + } + + return result; + } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index e7b91fb..4836eb8 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Identity; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; @@ -15,12 +18,20 @@ namespace TakeoutSaaS.Application.Identity.Services; /// public sealed class AdminAuthService( IIdentityUserRepository userRepository, + IUserRoleRepository userRoleRepository, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository, + IRolePermissionRepository rolePermissionRepository, IPasswordHasher passwordHasher, IJwtTokenService jwtTokenService, IRefreshTokenStore refreshTokenStore, ITenantProvider tenantProvider) : IAdminAuthService { private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; + private readonly IRoleRepository _roleRepository = roleRepository; + private readonly IPermissionRepository _permissionRepository = permissionRepository; + private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository; /// /// 管理后台登录:验证账号密码并生成令牌。 @@ -43,7 +54,7 @@ public sealed class AdminAuthService( } // 3. 构建用户档案并生成令牌 - var profile = BuildProfile(user); + var profile = await BuildProfileAsync(user, cancellationToken); return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } @@ -71,7 +82,7 @@ public sealed class AdminAuthService( await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); // 4. 生成新的令牌对 - var profile = BuildProfile(user); + var profile = await BuildProfileAsync(user, cancellationToken); return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } @@ -87,7 +98,7 @@ public sealed class AdminAuthService( var user = await userRepository.FindByIdAsync(userId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); - return BuildProfile(user); + return await BuildProfileAsync(user, cancellationToken); } /// @@ -102,6 +113,9 @@ public sealed class AdminAuthService( return null; } + var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); + var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + return new UserPermissionDto { UserId = user.Id, @@ -109,8 +123,8 @@ public sealed class AdminAuthService( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = roleCodes, + Permissions = permissionCodes, CreatedAt = user.CreatedAt }; } @@ -147,6 +161,7 @@ public sealed class AdminAuthService( .Take(pageSize) .ToList(); + var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken); var items = paged.Select(user => new UserPermissionDto { UserId = user.Id, @@ -154,24 +169,116 @@ public sealed class AdminAuthService( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = resolved[user.Id].roles, + Permissions = resolved[user.Id].permissions, CreatedAt = user.CreatedAt }).ToList(); return new PagedResult(items, page, pageSize, users.Count); } - private static CurrentUserProfile BuildProfile(IdentityUser user) - => new() + private async Task BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken) + { + var tenantId = user.TenantId; + var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); + var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + + return new CurrentUserProfile { UserId = user.Id, Account = user.Account, DisplayName = user.DisplayName, TenantId = user.TenantId, MerchantId = user.MerchantId, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = roles, + Permissions = permissions, Avatar = user.Avatar }; + } + + private async Task ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } + + private async Task ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + if (permissionIds.Length == 0) + { + return Array.Empty(); + } + + var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } + + private async Task> ResolveRolesAndPermissionsAsync( + long tenantId, + IReadOnlyCollection users, + CancellationToken cancellationToken) + { + var userIds = users.Select(x => x.Id).ToArray(); + var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); + + var roles = roleIds.Length == 0 + ? Array.Empty() + : await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); + + var rolePermissions = roleIds.Length == 0 + ? Array.Empty() + : await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + var permissions = permissionIds.Length == 0 + ? Array.Empty() + : await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); + + var rolePermissionsLookup = rolePermissions + .GroupBy(rp => rp.RoleId) + .ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer.Default); + + var result = new Dictionary(); + foreach (var userId in userIds) + { + var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray(); + var roleCodes = rolesForUser + .Select(rid => roleCodeMap.GetValueOrDefault(rid)) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Select(c => c!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var permissionCodes = rolesForUser + .SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty()) + .Select(pid => permissionCodeMap.GetValueOrDefault(pid)) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(code => code!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + result[userId] = (roleCodes, permissionCodes); + } + + return result; + } } diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs index 6080712..2bed359 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs @@ -27,16 +27,6 @@ public sealed class IdentityUser : MultiTenantEntityBase /// public long? MerchantId { get; set; } - /// - /// 角色集合。 - /// - public string[] Roles { get; set; } = Array.Empty(); - - /// - /// 权限集合。 - /// - public string[] Permissions { get; set; } = Array.Empty(); - /// /// 头像地址。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs new file mode 100644 index 0000000..53a4d0b --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs @@ -0,0 +1,24 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 权限定义。 +/// +public sealed class Permission : MultiTenantEntityBase +{ + /// + /// 权限名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 权限编码(租户内唯一)。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs new file mode 100644 index 0000000..356d8bc --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs @@ -0,0 +1,24 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 角色定义。 +/// +public sealed class Role : MultiTenantEntityBase +{ + /// + /// 角色名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 角色编码(租户内唯一)。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs new file mode 100644 index 0000000..55ac3e0 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs @@ -0,0 +1,19 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 角色-权限关系。 +/// +public sealed class RolePermission : MultiTenantEntityBase +{ + /// + /// 角色 ID。 + /// + public long RoleId { get; set; } + + /// + /// 权限 ID。 + /// + public long PermissionId { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs new file mode 100644 index 0000000..eeb147c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs @@ -0,0 +1,19 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 用户-角色关系。 +/// +public sealed class UserRole : MultiTenantEntityBase +{ + /// + /// 用户 ID。 + /// + public long UserId { get; set; } + + /// + /// 角色 ID。 + /// + public long RoleId { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs index 80d0a1e..5b809cf 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -28,4 +28,9 @@ public interface IIdentityUserRepository /// 可选关键字(账号/名称)。 /// 取消标记。 Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + + /// + /// 获取指定租户、用户集合对应的用户(只读)。 + /// + Task> GetByIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs new file mode 100644 index 0000000..2bf97a0 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 权限仓储。 +/// +public interface IPermissionRepository +{ + Task FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default); + Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default); + Task> GetByIdsAsync(long tenantId, IEnumerable permissionIds, CancellationToken cancellationToken = default); + Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + Task AddAsync(Permission permission, CancellationToken cancellationToken = default); + Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default); + Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs new file mode 100644 index 0000000..6ace0ce --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 角色-权限关系仓储。 +/// +public interface IRolePermissionRepository +{ + Task> GetByRoleIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable permissionIds, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs new file mode 100644 index 0000000..822266e --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 角色仓储。 +/// +public interface IRoleRepository +{ + Task FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default); + Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default); + Task> GetByIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + Task AddAsync(Role role, CancellationToken cancellationToken = default); + Task UpdateAsync(Role role, CancellationToken cancellationToken = default); + Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs new file mode 100644 index 0000000..aa9b9c8 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 用户-角色关系仓储。 +/// +public interface IUserRoleRepository +{ + Task> GetByUserIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default); + Task> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default); + Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs index 192b3a5..0c8dc63 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs @@ -51,6 +51,10 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index e1f657d..1f2bc95 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -34,4 +34,10 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde return await query.ToListAsync(cancellationToken); } + + public Task> GetByIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) + => dbContext.IdentityUsers.AsNoTracking() + .Where(x => x.TenantId == tenantId && userIds.Contains(x.Id)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs new file mode 100644 index 0000000..c07f15d --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 权限仓储。 +/// +public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermissionRepository +{ + public Task FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken); + + public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken); + + public Task> GetByIdsAsync(long tenantId, IEnumerable permissionIds, CancellationToken cancellationToken = default) + => dbContext.Permissions.AsNoTracking() + .Where(x => x.TenantId == tenantId && permissionIds.Contains(x.Id)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) + { + var query = dbContext.Permissions.AsNoTracking().Where(x => x.TenantId == tenantId); + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalized = keyword.Trim(); + query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); + } + + return query.ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + } + + public Task AddAsync(Permission permission, CancellationToken cancellationToken = default) + { + dbContext.Permissions.Add(permission); + return Task.CompletedTask; + } + + public Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default) + { + dbContext.Permissions.Update(permission); + return Task.CompletedTask; + } + + public async Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) + { + var entity = await dbContext.Permissions.FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken); + if (entity != null) + { + dbContext.Permissions.Remove(entity); + } + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs new file mode 100644 index 0000000..0409fff --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 角色-权限仓储。 +/// +public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IRolePermissionRepository +{ + public Task> GetByRoleIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) + => dbContext.RolePermissions.AsNoTracking() + .Where(x => x.TenantId == tenantId && roleIds.Contains(x.RoleId)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable permissionIds, CancellationToken cancellationToken = default) + { + var existing = await dbContext.RolePermissions + .Where(x => x.TenantId == tenantId && x.RoleId == roleId) + .ToListAsync(cancellationToken); + + dbContext.RolePermissions.RemoveRange(existing); + + var toAdd = permissionIds.Distinct().Select(permissionId => new RolePermission + { + TenantId = tenantId, + RoleId = roleId, + PermissionId = permissionId + }); + + await dbContext.RolePermissions.AddRangeAsync(toAdd, cancellationToken); + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs new file mode 100644 index 0000000..a6c43c4 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 角色仓储。 +/// +public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleRepository +{ + public Task FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken); + + public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken); + + public Task> GetByIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) + => dbContext.Roles.AsNoTracking() + .Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) + { + var query = dbContext.Roles.AsNoTracking().Where(x => x.TenantId == tenantId); + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalized = keyword.Trim(); + query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); + } + + return query.ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + } + + public Task AddAsync(Role role, CancellationToken cancellationToken = default) + { + dbContext.Roles.Add(role); + return Task.CompletedTask; + } + + public Task UpdateAsync(Role role, CancellationToken cancellationToken = default) + { + dbContext.Roles.Update(role); + return Task.CompletedTask; + } + + public async Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) + { + var entity = await dbContext.Roles.FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken); + if (entity != null) + { + dbContext.Roles.Remove(entity); + } + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs new file mode 100644 index 0000000..bf3c334 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 用户-角色仓储。 +/// +public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRoleRepository +{ + public Task> GetByUserIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) + => dbContext.UserRoles.AsNoTracking() + .Where(x => x.TenantId == tenantId && userIds.Contains(x.UserId)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public Task> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default) + => dbContext.UserRoles.AsNoTracking() + .Where(x => x.TenantId == tenantId && x.UserId == userId) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable roleIds, CancellationToken cancellationToken = default) + { + var existing = await dbContext.UserRoles + .Where(x => x.TenantId == tenantId && x.UserId == userId) + .ToListAsync(cancellationToken); + + dbContext.UserRoles.RemoveRange(existing); + + var toAdd = roleIds.Distinct().Select(roleId => new UserRole + { + TenantId = tenantId, + UserId = userId, + RoleId = roleId + }); + + await dbContext.UserRoles.AddRangeAsync(toAdd, cancellationToken); + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index b6ce20f..a9246dd 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -7,6 +9,10 @@ using Microsoft.Extensions.Options; using TakeoutSaaS.Infrastructure.Identity.Options; using TakeoutSaaS.Shared.Abstractions.Tenancy; 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 DomainUserRole = TakeoutSaaS.Domain.Identity.Entities.UserRole; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -47,9 +53,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger DisplayName = userOptions.DisplayName, TenantId = userOptions.TenantId, MerchantId = userOptions.MerchantId, - Avatar = null, - Roles = roles, - Permissions = permissions, + Avatar = null }; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); context.IdentityUsers.Add(user); @@ -60,11 +64,94 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger user.DisplayName = userOptions.DisplayName; user.TenantId = userOptions.TenantId; user.MerchantId = userOptions.MerchantId; - user.Roles = roles; - user.Permissions = permissions; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); logger.LogInformation("已更新后台账号 {Account}", user.Account); } + + // 确保角色存在 + var existingRoles = await context.Roles + .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) + .ToListAsync(cancellationToken); + var existingRoleCodes = existingRoles.Select(r => r.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var code in roles) + { + if (existingRoleCodes.Contains(code)) + { + continue; + } + + context.Roles.Add(new DomainRole + { + TenantId = userOptions.TenantId, + Code = code, + Name = code, + Description = $"Seed role {code}" + }); + } + + // 确保权限存在 + var existingPermissions = await context.Permissions + .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) + .ToListAsync(cancellationToken); + var existingPermissionCodes = existingPermissions.Select(p => p.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var code in permissions) + { + if (existingPermissionCodes.Contains(code)) + { + continue; + } + + context.Permissions.Add(new DomainPermission + { + TenantId = userOptions.TenantId, + Code = code, + Name = code, + Description = $"Seed permission {code}" + }); + } + + await context.SaveChangesAsync(cancellationToken); + + // 重新加载角色/权限以获取 Id + var roleEntities = await context.Roles + .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) + .ToListAsync(cancellationToken); + var permissionEntities = await context.Permissions + .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) + .ToListAsync(cancellationToken); + + // 重置用户角色 + var existingUserRoles = await context.UserRoles + .Where(ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id) + .ToListAsync(cancellationToken); + context.UserRoles.RemoveRange(existingUserRoles); + + var roleIds = roleEntities.Select(r => r.Id).Distinct().ToArray(); + var userRoles = roleIds.Select(roleId => new DomainUserRole + { + TenantId = userOptions.TenantId, + UserId = user.Id, + RoleId = roleId + }); + await context.UserRoles.AddRangeAsync(userRoles, cancellationToken); + + // 为种子角色绑定种子权限 + if (permissions.Length > 0 && roleIds.Length > 0) + { + var permissionIds = permissionEntities.Select(p => p.Id).Distinct().ToArray(); + var existingRolePermissions = await context.RolePermissions + .Where(rp => rp.TenantId == userOptions.TenantId && roleIds.Contains(rp.RoleId)) + .ToListAsync(cancellationToken); + context.RolePermissions.RemoveRange(existingRolePermissions); + + var newRolePermissions = roleIds.SelectMany(roleId => permissionIds.Select(permissionId => new DomainRolePermission + { + TenantId = userOptions.TenantId, + RoleId = roleId, + PermissionId = permissionId + })); + await context.RolePermissions.AddRangeAsync(newRolePermissions, cancellationToken); + } } await context.SaveChangesAsync(cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 7f98396..dfe4d4f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -1,8 +1,6 @@ using System.Linq; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; @@ -31,6 +29,26 @@ public sealed class IdentityDbContext( /// public DbSet MiniUsers => Set(); + /// + /// 角色集合。 + /// + public DbSet Roles => Set(); + + /// + /// 权限集合。 + /// + public DbSet Permissions => Set(); + + /// + /// 用户-角色关系。 + /// + public DbSet UserRoles => Set(); + + /// + /// 角色-权限关系。 + /// + public DbSet RolePermissions => Set(); + /// /// 配置实体模型。 /// @@ -40,6 +58,10 @@ public sealed class IdentityDbContext( base.OnModelCreating(modelBuilder); ConfigureIdentityUser(modelBuilder.Entity()); ConfigureMiniUser(modelBuilder.Entity()); + ConfigureRole(modelBuilder.Entity()); + ConfigurePermission(modelBuilder.Entity()); + ConfigureUserRole(modelBuilder.Entity()); + ConfigureRolePermission(modelBuilder.Entity()); ApplyTenantQueryFilters(modelBuilder); } @@ -59,23 +81,6 @@ public sealed class IdentityDbContext( ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - var converter = new ValueConverter( - v => string.Join(',', v), - v => string.IsNullOrWhiteSpace(v) ? Array.Empty() : v.Split(',', StringSplitOptions.RemoveEmptyEntries)); - - var comparer = new ValueComparer( - (l, r) => (l == null && r == null) || (l != null && r != null && Enumerable.SequenceEqual(l, r)), - v => v.Aggregate(0, (current, item) => HashCode.Combine(current, item.GetHashCode())), - v => v.ToArray()); - - builder.Property(x => x.Roles) - .HasConversion(converter) - .Metadata.SetValueComparer(comparer); - - builder.Property(x => x.Permissions) - .HasConversion(converter) - .Metadata.SetValueComparer(comparer); - builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.TenantId, x.Account }).IsUnique(); } @@ -99,4 +104,58 @@ public sealed class IdentityDbContext( builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.TenantId, x.OpenId }).IsUnique(); } + + private static void ConfigureRole(EntityTypeBuilder builder) + { + builder.ToTable("roles"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Code).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(256); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigurePermission(EntityTypeBuilder builder) + { + builder.ToTable("permissions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Code).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(256); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigureUserRole(EntityTypeBuilder builder) + { + builder.ToTable("user_roles"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.UserId).IsRequired(); + builder.Property(x => x.RoleId).IsRequired(); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique(); + } + + private static void ConfigureRolePermission(EntityTypeBuilder builder) + { + builder.ToTable("role_permissions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.RoleId).IsRequired(); + builder.Property(x => x.PermissionId).IsRequired(); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique(); + } }