feat: 重构 RBAC1 角色权限模型
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
using TakeoutSaaS.Application.Identity.Queries;
|
using TakeoutSaaS.Application.Identity.Queries;
|
||||||
@@ -11,10 +13,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetUserPermissionsQueryHandler(
|
public sealed class GetUserPermissionsQueryHandler(
|
||||||
IIdentityUserRepository identityUserRepository,
|
IIdentityUserRepository identityUserRepository,
|
||||||
|
IUserRoleRepository userRoleRepository,
|
||||||
|
IRoleRepository roleRepository,
|
||||||
|
IPermissionRepository permissionRepository,
|
||||||
|
IRolePermissionRepository rolePermissionRepository,
|
||||||
ITenantProvider tenantProvider)
|
ITenantProvider tenantProvider)
|
||||||
: IRequestHandler<GetUserPermissionsQuery, UserPermissionDto?>
|
: IRequestHandler<GetUserPermissionsQuery, UserPermissionDto?>
|
||||||
{
|
{
|
||||||
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
|
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;
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -27,6 +37,9 @@ public sealed class GetUserPermissionsQueryHandler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
|
||||||
|
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
|
||||||
|
|
||||||
return new UserPermissionDto
|
return new UserPermissionDto
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
@@ -34,9 +47,42 @@ public sealed class GetUserPermissionsQueryHandler(
|
|||||||
MerchantId = user.MerchantId,
|
MerchantId = user.MerchantId,
|
||||||
Account = user.Account,
|
Account = user.Account,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
Roles = user.Roles,
|
Roles = roleCodes,
|
||||||
Permissions = user.Permissions,
|
Permissions = permissionCodes,
|
||||||
CreatedAt = user.CreatedAt
|
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 System.Linq;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
@@ -13,10 +15,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SearchUserPermissionsQueryHandler(
|
public sealed class SearchUserPermissionsQueryHandler(
|
||||||
IIdentityUserRepository identityUserRepository,
|
IIdentityUserRepository identityUserRepository,
|
||||||
|
IUserRoleRepository userRoleRepository,
|
||||||
|
IRoleRepository roleRepository,
|
||||||
|
IPermissionRepository permissionRepository,
|
||||||
|
IRolePermissionRepository rolePermissionRepository,
|
||||||
ITenantProvider tenantProvider)
|
ITenantProvider tenantProvider)
|
||||||
: IRequestHandler<SearchUserPermissionsQuery, PagedResult<UserPermissionDto>>
|
: IRequestHandler<SearchUserPermissionsQuery, PagedResult<UserPermissionDto>>
|
||||||
{
|
{
|
||||||
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
|
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;
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -31,6 +41,7 @@ public sealed class SearchUserPermissionsQueryHandler(
|
|||||||
.Take(request.PageSize)
|
.Take(request.PageSize)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
|
||||||
var items = paged.Select(user => new UserPermissionDto
|
var items = paged.Select(user => new UserPermissionDto
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
@@ -38,8 +49,8 @@ public sealed class SearchUserPermissionsQueryHandler(
|
|||||||
MerchantId = user.MerchantId,
|
MerchantId = user.MerchantId,
|
||||||
Account = user.Account,
|
Account = user.Account,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
Roles = user.Roles,
|
Roles = resolved[user.Id].roles,
|
||||||
Permissions = user.Permissions,
|
Permissions = resolved[user.Id].permissions,
|
||||||
CreatedAt = user.CreatedAt
|
CreatedAt = user.CreatedAt
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@@ -64,4 +75,57 @@ public sealed class SearchUserPermissionsQueryHandler(
|
|||||||
: users.OrderBy(x => x.CreatedAt)
|
: 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 Microsoft.AspNetCore.Identity;
|
||||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
@@ -15,12 +18,20 @@ namespace TakeoutSaaS.Application.Identity.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AdminAuthService(
|
public sealed class AdminAuthService(
|
||||||
IIdentityUserRepository userRepository,
|
IIdentityUserRepository userRepository,
|
||||||
|
IUserRoleRepository userRoleRepository,
|
||||||
|
IRoleRepository roleRepository,
|
||||||
|
IPermissionRepository permissionRepository,
|
||||||
|
IRolePermissionRepository rolePermissionRepository,
|
||||||
IPasswordHasher<IdentityUser> passwordHasher,
|
IPasswordHasher<IdentityUser> passwordHasher,
|
||||||
IJwtTokenService jwtTokenService,
|
IJwtTokenService jwtTokenService,
|
||||||
IRefreshTokenStore refreshTokenStore,
|
IRefreshTokenStore refreshTokenStore,
|
||||||
ITenantProvider tenantProvider) : IAdminAuthService
|
ITenantProvider tenantProvider) : IAdminAuthService
|
||||||
{
|
{
|
||||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
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>
|
/// <summary>
|
||||||
/// 管理后台登录:验证账号密码并生成令牌。
|
/// 管理后台登录:验证账号密码并生成令牌。
|
||||||
@@ -43,7 +54,7 @@ public sealed class AdminAuthService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 构建用户档案并生成令牌
|
// 3. 构建用户档案并生成令牌
|
||||||
var profile = BuildProfile(user);
|
var profile = await BuildProfileAsync(user, cancellationToken);
|
||||||
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +82,7 @@ public sealed class AdminAuthService(
|
|||||||
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
|
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
|
||||||
|
|
||||||
// 4. 生成新的令牌对
|
// 4. 生成新的令牌对
|
||||||
var profile = BuildProfile(user);
|
var profile = await BuildProfileAsync(user, cancellationToken);
|
||||||
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +98,7 @@ public sealed class AdminAuthService(
|
|||||||
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
|
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||||
|
|
||||||
return BuildProfile(user);
|
return await BuildProfileAsync(user, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -102,6 +113,9 @@ public sealed class AdminAuthService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
|
||||||
|
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
|
||||||
|
|
||||||
return new UserPermissionDto
|
return new UserPermissionDto
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
@@ -109,8 +123,8 @@ public sealed class AdminAuthService(
|
|||||||
MerchantId = user.MerchantId,
|
MerchantId = user.MerchantId,
|
||||||
Account = user.Account,
|
Account = user.Account,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
Roles = user.Roles,
|
Roles = roleCodes,
|
||||||
Permissions = user.Permissions,
|
Permissions = permissionCodes,
|
||||||
CreatedAt = user.CreatedAt
|
CreatedAt = user.CreatedAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -147,6 +161,7 @@ public sealed class AdminAuthService(
|
|||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
|
||||||
var items = paged.Select(user => new UserPermissionDto
|
var items = paged.Select(user => new UserPermissionDto
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
@@ -154,24 +169,116 @@ public sealed class AdminAuthService(
|
|||||||
MerchantId = user.MerchantId,
|
MerchantId = user.MerchantId,
|
||||||
Account = user.Account,
|
Account = user.Account,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
Roles = user.Roles,
|
Roles = resolved[user.Id].roles,
|
||||||
Permissions = user.Permissions,
|
Permissions = resolved[user.Id].permissions,
|
||||||
CreatedAt = user.CreatedAt
|
CreatedAt = user.CreatedAt
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
return new PagedResult<UserPermissionDto>(items, page, pageSize, users.Count);
|
return new PagedResult<UserPermissionDto>(items, page, pageSize, users.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CurrentUserProfile BuildProfile(IdentityUser user)
|
private async Task<CurrentUserProfile> BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken)
|
||||||
=> new()
|
{
|
||||||
|
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,
|
UserId = user.Id,
|
||||||
Account = user.Account,
|
Account = user.Account,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
TenantId = user.TenantId,
|
TenantId = user.TenantId,
|
||||||
MerchantId = user.MerchantId,
|
MerchantId = user.MerchantId,
|
||||||
Roles = user.Roles,
|
Roles = roles,
|
||||||
Permissions = user.Permissions,
|
Permissions = permissions,
|
||||||
Avatar = user.Avatar
|
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>
|
/// </summary>
|
||||||
public long? MerchantId { get; set; }
|
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>
|
||||||
/// 头像地址。
|
/// 头像地址。
|
||||||
/// </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="keyword">可选关键字(账号/名称)。</param>
|
||||||
/// <param name="cancellationToken">取消标记。</param>
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
Task<IReadOnlyList<IdentityUser>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default);
|
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<IIdentityUserRepository, EfIdentityUserRepository>();
|
||||||
services.AddScoped<IMiniUserRepository, EfMiniUserRepository>();
|
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<IJwtTokenService, JwtTokenService>();
|
||||||
services.AddScoped<IRefreshTokenStore, RedisRefreshTokenStore>();
|
services.AddScoped<IRefreshTokenStore, RedisRefreshTokenStore>();
|
||||||
services.AddScoped<ILoginRateLimiter, RedisLoginRateLimiter>();
|
services.AddScoped<ILoginRateLimiter, RedisLoginRateLimiter>();
|
||||||
|
|||||||
@@ -34,4 +34,10 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
|||||||
|
|
||||||
return await query.ToListAsync(cancellationToken);
|
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.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -7,6 +9,10 @@ using Microsoft.Extensions.Options;
|
|||||||
using TakeoutSaaS.Infrastructure.Identity.Options;
|
using TakeoutSaaS.Infrastructure.Identity.Options;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser;
|
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;
|
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||||
|
|
||||||
@@ -47,9 +53,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
|
|||||||
DisplayName = userOptions.DisplayName,
|
DisplayName = userOptions.DisplayName,
|
||||||
TenantId = userOptions.TenantId,
|
TenantId = userOptions.TenantId,
|
||||||
MerchantId = userOptions.MerchantId,
|
MerchantId = userOptions.MerchantId,
|
||||||
Avatar = null,
|
Avatar = null
|
||||||
Roles = roles,
|
|
||||||
Permissions = permissions,
|
|
||||||
};
|
};
|
||||||
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
|
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
|
||||||
context.IdentityUsers.Add(user);
|
context.IdentityUsers.Add(user);
|
||||||
@@ -60,11 +64,94 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
|
|||||||
user.DisplayName = userOptions.DisplayName;
|
user.DisplayName = userOptions.DisplayName;
|
||||||
user.TenantId = userOptions.TenantId;
|
user.TenantId = userOptions.TenantId;
|
||||||
user.MerchantId = userOptions.MerchantId;
|
user.MerchantId = userOptions.MerchantId;
|
||||||
user.Roles = roles;
|
|
||||||
user.Permissions = permissions;
|
|
||||||
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
|
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
|
||||||
logger.LogInformation("已更新后台账号 {Account}", user.Account);
|
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);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using TakeoutSaaS.Domain.Identity.Entities;
|
using TakeoutSaaS.Domain.Identity.Entities;
|
||||||
using TakeoutSaaS.Infrastructure.Common.Persistence;
|
using TakeoutSaaS.Infrastructure.Common.Persistence;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||||
@@ -31,6 +29,26 @@ public sealed class IdentityDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<MiniUser> MiniUsers => Set<MiniUser>();
|
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>
|
||||||
/// 配置实体模型。
|
/// 配置实体模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -40,6 +58,10 @@ public sealed class IdentityDbContext(
|
|||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
ConfigureIdentityUser(modelBuilder.Entity<IdentityUser>());
|
ConfigureIdentityUser(modelBuilder.Entity<IdentityUser>());
|
||||||
ConfigureMiniUser(modelBuilder.Entity<MiniUser>());
|
ConfigureMiniUser(modelBuilder.Entity<MiniUser>());
|
||||||
|
ConfigureRole(modelBuilder.Entity<Role>());
|
||||||
|
ConfigurePermission(modelBuilder.Entity<Permission>());
|
||||||
|
ConfigureUserRole(modelBuilder.Entity<UserRole>());
|
||||||
|
ConfigureRolePermission(modelBuilder.Entity<RolePermission>());
|
||||||
ApplyTenantQueryFilters(modelBuilder);
|
ApplyTenantQueryFilters(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,23 +81,6 @@ public sealed class IdentityDbContext(
|
|||||||
ConfigureAuditableEntity(builder);
|
ConfigureAuditableEntity(builder);
|
||||||
ConfigureSoftDeleteEntity(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 => x.TenantId);
|
||||||
builder.HasIndex(x => new { x.TenantId, x.Account }).IsUnique();
|
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 => x.TenantId);
|
||||||
builder.HasIndex(x => new { x.TenantId, x.OpenId }).IsUnique();
|
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