refactor: 收紧角色与商户跨租户能力

This commit is contained in:
root
2026-01-29 14:52:25 +00:00
parent a0b77d4847
commit 41cfd2e2e8
9 changed files with 120 additions and 75 deletions

View File

@@ -27,7 +27,7 @@ public sealed class GetMerchantListQuery : IRequest<PagedResult<MerchantListItem
public OperatingMode? OperatingMode { get; init; }
/// <summary>
/// 租户过滤(管理员可用)。
/// 租户过滤(可选,默认当前租户;禁止跨租户)。
/// </summary>
public long? TenantId { get; init; }

View File

@@ -1,6 +1,8 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -22,9 +24,21 @@ public sealed class BindRolePermissionsCommandHandler(
public async Task<bool> Handle(BindRolePermissionsCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 2. (空行后) 禁止跨租户操作
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户操作角色权限");
}
// 3. (空行后) 覆盖式绑定权限
var tenantId = currentTenantId;
// 2. 覆盖式绑定权限
var distinctPermissionIds = request.PermissionIds
.Where(id => id > 0)
.Distinct()
@@ -33,7 +47,7 @@ public sealed class BindRolePermissionsCommandHandler(
await rolePermissionRepository.ReplaceRolePermissionsAsync(tenantId, request.RoleId, distinctPermissionIds, cancellationToken);
await rolePermissionRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果
// 4. (空行后) 返回执行结果
return true;
}
}

View File

@@ -25,10 +25,23 @@ public sealed class CreateRoleCommandHandler(
/// <returns>创建后的角色 DTO。</returns>
public async Task<RoleDto> Handle(CreateRoleCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
// 1. 获取租户上下文并校验跨租户
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 2. 归一化输入并校验唯一
// 2. (空行后) 禁止跨租户创建
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建角色");
}
// 3. (空行后) 使用当前租户创建角色
var tenantId = currentTenantId;
// 4. (空行后) 归一化输入并校验唯一
var name = request.Name?.Trim() ?? string.Empty;
var code = request.Code?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(code))
@@ -42,7 +55,7 @@ public sealed class CreateRoleCommandHandler(
throw new BusinessException(ErrorCodes.Conflict, "角色编码已存在");
}
// 3. 构建角色实体
// 5. (空行后) 构建角色实体
var role = new Role
{
TenantId = tenantId,
@@ -51,11 +64,11 @@ public sealed class CreateRoleCommandHandler(
Description = request.Description
};
// 4. 持久化
// 6. (空行后) 持久化
await roleRepository.AddAsync(role, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
// 7. (空行后) 返回 DTO
return new RoleDto
{
Id = role.Id,

View File

@@ -1,6 +1,8 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -22,13 +24,25 @@ public sealed class DeleteRoleCommandHandler(
public async Task<bool> Handle(DeleteRoleCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 2. (空行后) 禁止跨租户操作
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户操作角色");
}
// 3. (空行后) 删除角色
var tenantId = currentTenantId;
// 2. 删除角色
await roleRepository.DeleteAsync(request.RoleId, tenantId, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果
// 4. (空行后) 返回执行结果
return true;
}
}

View File

@@ -2,6 +2,8 @@ using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -19,24 +21,37 @@ public sealed class RoleDetailQueryHandler(
/// <inheritdoc />
public async Task<RoleDetailDto?> Handle(RoleDetailQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询角色
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
// 1. 获取租户上下文并校验跨租户
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 2. (空行后) 禁止跨租户查询
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询角色详情");
}
// 3. (空行后) 查询角色
var tenantId = currentTenantId;
var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken);
if (role is null)
{
return null;
}
// 2. 查询角色权限关系
// 4. (空行后) 查询角色权限关系
var relations = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken);
var permissionIds = relations.Select(x => x.PermissionId).ToArray();
// 3. 拉取权限实体
// 5. (空行后) 拉取权限实体
var permissions = permissionIds.Length == 0
? Array.Empty<Domain.Identity.Entities.Permission>()
: await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
// 4. 映射 DTO
// 6. (空行后) 映射 DTO
var permissionDtos = permissions
.Select(x => new PermissionDto
{

View File

@@ -2,6 +2,8 @@ using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
@@ -23,11 +25,24 @@ public sealed class SearchRolesQueryHandler(
/// <returns>分页结果。</returns>
public async Task<PagedResult<RoleDto>> Handle(SearchRolesQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询角色
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
// 1. 获取租户上下文并校验跨租户
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 2. (空行后) 禁止跨租户查询
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询角色");
}
// 3. (空行后) 查询角色列表
var tenantId = currentTenantId;
var roles = await roleRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
// 2. 排序
// 4. (空行后) 排序
var sorted = request.SortBy?.ToLowerInvariant() switch
{
"name" => request.SortDescending
@@ -38,13 +53,13 @@ public sealed class SearchRolesQueryHandler(
: roles.OrderBy(x => x.CreatedAt)
};
// 3. 分页
// 5. (空行后) 分页
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 4. 映射 DTO
// 6. (空行后) 映射 DTO
var items = paged.Select(role => new RoleDto
{
Id = role.Id,
@@ -54,7 +69,7 @@ public sealed class SearchRolesQueryHandler(
Description = role.Description
}).ToList();
// 5. 返回分页结果
// 7. (空行后) 返回分页结果
return new PagedResult<RoleDto>(items, request.Page, request.PageSize, roles.Count);
}
}

View File

@@ -2,6 +2,8 @@ using MediatR;
using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -22,23 +24,36 @@ public sealed class UpdateRoleCommandHandler(
/// <returns>更新后的角色 DTO 或 null。</returns>
public async Task<RoleDto?> Handle(UpdateRoleCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询角色
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
// 1. 获取租户上下文并校验跨租户
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 2. (空行后) 禁止跨租户更新
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户更新角色");
}
// 3. (空行后) 查询角色
var tenantId = currentTenantId;
var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken);
if (role == null)
{
return null;
}
// 2. 更新字段
// 4. (空行后) 更新字段
role.Name = request.Name;
role.Description = request.Description;
// 3. 持久化
// 5. (空行后) 持久化
await roleRepository.UpdateAsync(role, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
// 6. (空行后) 返回 DTO
return new RoleDto
{
Id = role.Id,