feat: 重构 RBAC1 角色权限模型
This commit is contained in:
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class GetUserPermissionsQueryHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetUserPermissionsQuery, UserPermissionDto?>
|
||||
{
|
||||
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;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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<string[]> 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<string>();
|
||||
}
|
||||
|
||||
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private async Task<string[]> 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<string>();
|
||||
}
|
||||
|
||||
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
if (permissionIds.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class SearchUserPermissionsQueryHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SearchUserPermissionsQuery, PagedResult<UserPermissionDto>>
|
||||
{
|
||||
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;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
||||
long tenantId,
|
||||
IReadOnlyCollection<Domain.Identity.Entities.IdentityUser> 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<Domain.Identity.Entities.Role>()
|
||||
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissions = roleIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.RolePermission>()
|
||||
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.Permission>()
|
||||
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
.GroupBy(rp => rp.RoleId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
|
||||
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<long>())
|
||||
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Select(code => code!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
result[userId] = (roleCodes, permissionCodes);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class AdminAuthService(
|
||||
IIdentityUserRepository userRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
IPasswordHasher<IdentityUser> 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;
|
||||
|
||||
/// <summary>
|
||||
/// 管理后台登录:验证账号密码并生成令牌。
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<UserPermissionDto>(items, page, pageSize, users.Count);
|
||||
}
|
||||
|
||||
private static CurrentUserProfile BuildProfile(IdentityUser user)
|
||||
=> new()
|
||||
private async Task<CurrentUserProfile> 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<string[]> 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<string>();
|
||||
}
|
||||
|
||||
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private async Task<string[]> 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<string>();
|
||||
}
|
||||
|
||||
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
if (permissionIds.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private async Task<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
||||
long tenantId,
|
||||
IReadOnlyCollection<IdentityUser> 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<Role>()
|
||||
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissions = roleIds.Length == 0
|
||||
? Array.Empty<RolePermission>()
|
||||
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<Permission>()
|
||||
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
.GroupBy(rp => rp.RoleId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
|
||||
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<long>())
|
||||
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Select(code => code!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
result[userId] = (roleCodes, permissionCodes);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +27,6 @@ public sealed class IdentityUser : MultiTenantEntityBase
|
||||
/// </summary>
|
||||
public long? MerchantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合。
|
||||
/// </summary>
|
||||
public string[] Roles { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 权限集合。
|
||||
/// </summary>
|
||||
public string[] Permissions { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 头像地址。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 权限定义。
|
||||
/// </summary>
|
||||
public sealed class Permission : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码(租户内唯一)。
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 描述。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
24
src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs
Normal file
24
src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 角色定义。
|
||||
/// </summary>
|
||||
public sealed class Role : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色编码(租户内唯一)。
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 描述。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 角色-权限关系。
|
||||
/// </summary>
|
||||
public sealed class RolePermission : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色 ID。
|
||||
/// </summary>
|
||||
public long RoleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限 ID。
|
||||
/// </summary>
|
||||
public long PermissionId { get; set; }
|
||||
}
|
||||
19
src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs
Normal file
19
src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 用户-角色关系。
|
||||
/// </summary>
|
||||
public sealed class UserRole : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID。
|
||||
/// </summary>
|
||||
public long RoleId { get; set; }
|
||||
}
|
||||
@@ -28,4 +28,9 @@ public interface IIdentityUserRepository
|
||||
/// <param name="keyword">可选关键字(账号/名称)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task<IReadOnlyList<IdentityUser>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定租户、用户集合对应的用户(只读)。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<IdentityUser>> GetByIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 权限仓储。
|
||||
/// </summary>
|
||||
public interface IPermissionRepository
|
||||
{
|
||||
Task<Permission?> FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Permission?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Permission>> GetByIdsAsync(long tenantId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Permission>> 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 角色-权限关系仓储。
|
||||
/// </summary>
|
||||
public interface IRolePermissionRepository
|
||||
{
|
||||
Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default);
|
||||
Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 角色仓储。
|
||||
/// </summary>
|
||||
public interface IRoleRepository
|
||||
{
|
||||
Task<Role?> FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Role?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Role>> GetByIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Role>> 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 用户-角色关系仓储。
|
||||
/// </summary>
|
||||
public interface IUserRoleRepository
|
||||
{
|
||||
Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<UserRole>> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default);
|
||||
Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -51,6 +51,10 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
services.AddScoped<IIdentityUserRepository, EfIdentityUserRepository>();
|
||||
services.AddScoped<IMiniUserRepository, EfMiniUserRepository>();
|
||||
services.AddScoped<IRoleRepository, EfRoleRepository>();
|
||||
services.AddScoped<IPermissionRepository, EfPermissionRepository>();
|
||||
services.AddScoped<IUserRoleRepository, EfUserRoleRepository>();
|
||||
services.AddScoped<IRolePermissionRepository, EfRolePermissionRepository>();
|
||||
services.AddScoped<IJwtTokenService, JwtTokenService>();
|
||||
services.AddScoped<IRefreshTokenStore, RedisRefreshTokenStore>();
|
||||
services.AddScoped<ILoginRateLimiter, RedisLoginRateLimiter>();
|
||||
|
||||
@@ -34,4 +34,10 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
|
||||
return await query.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<IdentityUser>> GetByIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && userIds.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<IdentityUser>)t.Result, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// EF 权限仓储。
|
||||
/// </summary>
|
||||
public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermissionRepository
|
||||
{
|
||||
public Task<Permission?> FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken);
|
||||
|
||||
public Task<Permission?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken);
|
||||
|
||||
public Task<IReadOnlyList<Permission>> GetByIdsAsync(long tenantId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && permissionIds.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, cancellationToken);
|
||||
|
||||
public Task<IReadOnlyList<Permission>> 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<Permission>)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);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// EF 角色-权限仓储。
|
||||
/// </summary>
|
||||
public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IRolePermissionRepository
|
||||
{
|
||||
public Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.RolePermissions.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && roleIds.Contains(x.RoleId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<RolePermission>)t.Result, cancellationToken);
|
||||
|
||||
public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable<long> 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);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// EF 角色仓储。
|
||||
/// </summary>
|
||||
public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleRepository
|
||||
{
|
||||
public Task<Role?> FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken);
|
||||
|
||||
public Task<Role?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken);
|
||||
|
||||
public Task<IReadOnlyList<Role>> GetByIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Roles.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<Role>)t.Result, cancellationToken);
|
||||
|
||||
public Task<IReadOnlyList<Role>> 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<Role>)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);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// EF 用户-角色仓储。
|
||||
/// </summary>
|
||||
public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRoleRepository
|
||||
{
|
||||
public Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.UserRoles.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && userIds.Contains(x.UserId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, cancellationToken);
|
||||
|
||||
public Task<IReadOnlyList<UserRole>> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.UserRoles.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.UserId == userId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, cancellationToken);
|
||||
|
||||
public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable<long> 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
/// </summary>
|
||||
public DbSet<MiniUser> MiniUsers => Set<MiniUser>();
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合。
|
||||
/// </summary>
|
||||
public DbSet<Role> Roles => Set<Role>();
|
||||
|
||||
/// <summary>
|
||||
/// 权限集合。
|
||||
/// </summary>
|
||||
public DbSet<Permission> Permissions => Set<Permission>();
|
||||
|
||||
/// <summary>
|
||||
/// 用户-角色关系。
|
||||
/// </summary>
|
||||
public DbSet<UserRole> UserRoles => Set<UserRole>();
|
||||
|
||||
/// <summary>
|
||||
/// 角色-权限关系。
|
||||
/// </summary>
|
||||
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
|
||||
|
||||
/// <summary>
|
||||
/// 配置实体模型。
|
||||
/// </summary>
|
||||
@@ -40,6 +58,10 @@ public sealed class IdentityDbContext(
|
||||
base.OnModelCreating(modelBuilder);
|
||||
ConfigureIdentityUser(modelBuilder.Entity<IdentityUser>());
|
||||
ConfigureMiniUser(modelBuilder.Entity<MiniUser>());
|
||||
ConfigureRole(modelBuilder.Entity<Role>());
|
||||
ConfigurePermission(modelBuilder.Entity<Permission>());
|
||||
ConfigureUserRole(modelBuilder.Entity<UserRole>());
|
||||
ConfigureRolePermission(modelBuilder.Entity<RolePermission>());
|
||||
ApplyTenantQueryFilters(modelBuilder);
|
||||
}
|
||||
|
||||
@@ -59,23 +81,6 @@ public sealed class IdentityDbContext(
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
|
||||
var converter = new ValueConverter<string[], string>(
|
||||
v => string.Join(',', v),
|
||||
v => string.IsNullOrWhiteSpace(v) ? Array.Empty<string>() : v.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||
|
||||
var comparer = new ValueComparer<string[]>(
|
||||
(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<Role> 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<Permission> 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<UserRole> 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<RolePermission> 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user