diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantListQuery.cs index 6998b15..ce4dfdc 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantListQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantListQuery.cs @@ -27,7 +27,7 @@ public sealed class GetMerchantListQuery : IRequest - /// 租户过滤(管理员可用)。 + /// 租户过滤(可选,默认当前租户;禁止跨租户)。 /// public long? TenantId { get; init; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs index 5200272..4928a72 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs @@ -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 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; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs index 232b8ab..1008782 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs @@ -25,10 +25,23 @@ public sealed class CreateRoleCommandHandler( /// 创建后的角色 DTO。 public async Task 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, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs index d1e0e4f..592df9a 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs @@ -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 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; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs index c2e0730..e958f7e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs @@ -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( /// public async Task 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() : await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); - // 4. 映射 DTO + // 6. (空行后) 映射 DTO var permissionDtos = permissions .Select(x => new PermissionDto { diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs index 2c247bb..0e0f575 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs @@ -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( /// 分页结果。 public async Task> 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(items, request.Page, request.PageSize, roles.Count); } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs index e8a6f49..01f7b67 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs @@ -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( /// 更新后的角色 DTO 或 null。 public async Task 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, diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs index 1dd5a0f..c1b972c 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs @@ -18,22 +18,6 @@ public interface IMerchantRepository /// 商户实体或 null。 Task FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); - /// - /// 依据标识获取商户(忽略租户过滤)。 - /// - /// 商户 ID。 - /// 取消标记。 - /// 商户实体或 null。 - Task FindByIdAsync(long merchantId, CancellationToken cancellationToken = default); - - /// - /// 依据租户标识获取商户(忽略租户过滤)。 - /// - /// 租户 ID。 - /// 取消标记。 - /// 商户实体或 null。 - Task FindByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default); - /// /// 按状态筛选商户列表。 /// @@ -44,16 +28,16 @@ public interface IMerchantRepository Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default); /// - /// 按条件筛选商户列表(支持跨租户)。 + /// 按条件筛选商户列表。 /// - /// 租户 ID,为 null 时查询全部租户。 + /// 租户 ID。 /// 状态过滤。 /// 经营模式过滤。 /// 关键词过滤。 /// 取消标记。 /// 商户集合。 Task> SearchAsync( - long? tenantId, + long tenantId, MerchantStatus? status, OperatingMode? operatingMode, string? keyword, diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs index 1a2454f..8a02952 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs @@ -25,26 +25,6 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog .FirstOrDefaultAsync(cancellationToken); } - /// - public Task FindByIdAsync(long merchantId, CancellationToken cancellationToken = default) - { - return context.Merchants - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.Id == merchantId) - .FirstOrDefaultAsync(cancellationToken); - } - - /// - public Task FindByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default) - { - return context.Merchants - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.TenantId == tenantId) - .FirstOrDefaultAsync(cancellationToken); - } - /// public async Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default) { @@ -238,22 +218,17 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog /// public async Task> SearchAsync( - long? tenantId, + long tenantId, MerchantStatus? status, OperatingMode? operatingMode, string? keyword, CancellationToken cancellationToken = default) { var query = context.Merchants - .IgnoreQueryFilters() .AsNoTracking() + .Where(x => x.TenantId == tenantId) .AsQueryable(); - if (tenantId.HasValue && tenantId.Value > 0) - { - query = query.Where(x => x.TenantId == tenantId.Value); - } - if (status.HasValue) { query = query.Where(x => x.Status == status.Value);