From a0b77d48471b7ddf137342b1899abb0769a038a9 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 14:51:56 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A6=81=E6=AD=A2=E8=B4=A6=E5=8D=95?= =?UTF-8?q?=E8=B7=A8=E7=A7=9F=E6=88=B7=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Billings/Dto/BillingDetailDto.cs | 2 +- .../App/Billings/Dto/BillingDtos.cs | 2 +- .../App/Billings/Dto/BillingListDto.cs | 2 +- .../App/Billings/Dto/BillingStatisticsDto.cs | 2 +- .../App/Billings/Dto/PaymentRecordDto.cs | 2 +- .../Handlers/GetBillingListQueryHandler.cs | 29 +++++++++++++---- .../GetBillingStatisticsQueryHandler.cs | 31 ++++++++++++++----- .../Billings/Queries/GetBillingListQuery.cs | 2 +- .../Queries/GetBillingStatisticsQuery.cs | 2 +- .../Repositories/ITenantBillingRepository.cs | 10 +++--- .../Repositories/TenantBillingRepository.cs | 4 +-- 11 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs index 02426cd..9d92bd3 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs @@ -5,7 +5,7 @@ using TakeoutSaaS.Shared.Abstractions.Serialization; namespace TakeoutSaaS.Application.App.Billings.Dto; /// -/// 账单详情 DTO(管理员端)。 +/// 账单详情 DTO(租户端)。 /// public sealed record BillingDetailDto { diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs index b14feab..9ad57bb 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs @@ -364,7 +364,7 @@ public sealed record PaymentRecordDto public sealed record BillingStatisticsDto { /// - /// 租户 ID(为空表示跨租户统计)。 + /// 租户 ID(当前租户)。 /// [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] public long? TenantId { get; init; } diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs index 2bea943..92e35b6 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs @@ -5,7 +5,7 @@ using TakeoutSaaS.Shared.Abstractions.Serialization; namespace TakeoutSaaS.Application.App.Billings.Dto; /// -/// 账单列表 DTO(管理员端列表展示)。 +/// 账单列表 DTO(租户端列表展示)。 /// public sealed record BillingListDto { diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs index fd00cb1..73b037b 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs @@ -9,7 +9,7 @@ namespace TakeoutSaaS.Application.App.Billings.Dto; public sealed record BillingStatisticsDto { /// - /// 租户 ID(可选,管理员可跨租户统计)。 + /// 租户 ID(当前租户)。 /// [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] public long? TenantId { get; init; } diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs index 0c4dd58..8e37c41 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs @@ -5,7 +5,7 @@ using TakeoutSaaS.Shared.Abstractions.Serialization; namespace TakeoutSaaS.Application.App.Billings.Dto; /// -/// 支付记录 DTO(管理员端)。 +/// 支付记录 DTO(租户端)。 /// public sealed record PaymentRecordDto { diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingListQueryHandler.cs index 894e644..f88c50a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingListQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingListQueryHandler.cs @@ -6,7 +6,9 @@ using TakeoutSaaS.Application.App.Billings.Queries; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Data; +using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Billings.Handlers; @@ -14,7 +16,8 @@ namespace TakeoutSaaS.Application.App.Billings.Handlers; /// 分页查询账单列表处理器。 /// public sealed class GetBillingListQueryHandler( - IDapperExecutor dapperExecutor) + IDapperExecutor dapperExecutor, + ITenantProvider tenantProvider) : IRequestHandler> { /// @@ -25,7 +28,21 @@ public sealed class GetBillingListQueryHandler( /// 分页账单列表 DTO。 public async Task> Handle(GetBillingListQuery request, CancellationToken cancellationToken) { - // 1. 参数规范化 + // 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, "禁止跨租户查询账单"); + } + var tenantId = currentTenantId; + + // 3. (空行后) 参数规范化 var page = request.PageNumber <= 0 ? 1 : request.PageNumber; var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize; var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim(); @@ -61,7 +78,7 @@ public sealed class GetBillingListQueryHandler( connection, BuildCountSql(), [ - ("tenantId", request.TenantId), + ("tenantId", tenantId), ("status", request.Status.HasValue ? (int)request.Status.Value : null), ("billingType", request.BillingType.HasValue ? (int)request.BillingType.Value : null), ("startDate", request.StartDate), @@ -78,7 +95,7 @@ public sealed class GetBillingListQueryHandler( connection, listSql, [ - ("tenantId", request.TenantId), + ("tenantId", tenantId), ("status", request.Status.HasValue ? (int)request.Status.Value : null), ("billingType", request.BillingType.HasValue ? (int)request.BillingType.Value : null), ("startDate", request.StartDate), @@ -145,7 +162,7 @@ public sealed class GetBillingListQueryHandler( from public.tenant_billing_statements b join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) + and b."TenantId" = @tenantId and (@status::int is null or b."Status" = @status) and (@billingType::int is null or b."BillingType" = @billingType) and (@startDate::timestamp with time zone is null or b."PeriodStart" >= @startDate) @@ -186,7 +203,7 @@ public sealed class GetBillingListQueryHandler( from public.tenant_billing_statements b join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) + and b."TenantId" = @tenantId and (@status::int is null or b."Status" = @status) and (@billingType::int is null or b."BillingType" = @billingType) and (@startDate::timestamp with time zone is null or b."PeriodStart" >= @startDate) diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingStatisticsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingStatisticsQueryHandler.cs index e5e3aa3..4410e6e 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingStatisticsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingStatisticsQueryHandler.cs @@ -6,6 +6,8 @@ using TakeoutSaaS.Application.App.Billings.Queries; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Data; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Billings.Handlers; @@ -13,7 +15,8 @@ namespace TakeoutSaaS.Application.App.Billings.Handlers; /// 查询账单统计数据处理器。 /// public sealed class GetBillingStatisticsQueryHandler( - IDapperExecutor dapperExecutor) + IDapperExecutor dapperExecutor, + ITenantProvider tenantProvider) : IRequestHandler { /// @@ -24,7 +27,21 @@ public sealed class GetBillingStatisticsQueryHandler( /// 账单统计数据 DTO。 public async Task Handle(GetBillingStatisticsQuery request, CancellationToken cancellationToken) { - // 1. 参数规范化 + // 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, "禁止跨租户统计账单"); + } + var tenantId = currentTenantId; + + // 3. (空行后) 参数规范化 var startDate = request.StartDate ?? DateTime.UtcNow.AddMonths(-1); var endDate = request.EndDate ?? DateTime.UtcNow; var groupBy = NormalizeGroupBy(request.GroupBy); @@ -40,7 +57,7 @@ public sealed class GetBillingStatisticsQueryHandler( connection, BuildSummarySql(), [ - ("tenantId", request.TenantId), + ("tenantId", tenantId), ("startDate", startDate), ("endDate", endDate), ("now", DateTime.UtcNow) @@ -64,7 +81,7 @@ public sealed class GetBillingStatisticsQueryHandler( connection, BuildTrendSql(groupBy), [ - ("tenantId", request.TenantId), + ("tenantId", tenantId), ("startDate", startDate), ("endDate", endDate) ]); @@ -86,7 +103,7 @@ public sealed class GetBillingStatisticsQueryHandler( // 2.3 组装 DTO return new BillingStatisticsDto { - TenantId = request.TenantId, + TenantId = tenantId, StartDate = startDate, EndDate = endDate, GroupBy = groupBy, @@ -138,7 +155,7 @@ public sealed class GetBillingStatisticsQueryHandler( ), 0)::numeric as "TotalOverdueAmount" from public.tenant_billing_statements b where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) + and b."TenantId" = @tenantId and b."PeriodStart" >= @startDate and b."PeriodEnd" <= @endDate; """; @@ -161,7 +178,7 @@ public sealed class GetBillingStatisticsQueryHandler( count(*)::int as "Count" from public.tenant_billing_statements b where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) + and b."TenantId" = @tenantId and b."PeriodStart" >= @startDate and b."PeriodEnd" <= @endDate group by 1 diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs index da964e7..7d3a7f9 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs @@ -11,7 +11,7 @@ namespace TakeoutSaaS.Application.App.Billings.Queries; public sealed record GetBillingListQuery : IRequest> { /// - /// 租户 ID(可选,管理员可查询所有租户)。 + /// 租户 ID(可选,默认当前租户;禁止跨租户)。 /// public long? TenantId { get; init; } diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs index 5927f57..f8e27dc 100644 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs @@ -9,7 +9,7 @@ namespace TakeoutSaaS.Application.App.Billings.Queries; public sealed record GetBillingStatisticsQuery : IRequest { /// - /// 租户 ID(可选,管理员可查询所有租户)。 + /// 租户 ID(可选,默认当前租户;禁止跨租户)。 /// public long? TenantId { get; init; } diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs index 7c88d2b..fa8831a 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs @@ -43,7 +43,7 @@ public interface ITenantBillingRepository Task FindByStatementNoAsync(long tenantId, string statementNo, CancellationToken cancellationToken = default); /// - /// 按账单编号获取账单(不限租户,管理员端使用)。 + /// 按账单编号获取账单(不限租户,系统任务使用)。 /// /// 账单编号。 /// 取消标记。 @@ -86,7 +86,7 @@ public interface ITenantBillingRepository Task> GetByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default); /// - /// 按 ID 列表批量获取账单(管理员端/批量操作场景)。 + /// 按 ID 列表批量获取账单(系统任务/批量操作场景)。 /// /// 账单 ID 列表。 /// 取消标记。 @@ -119,7 +119,7 @@ public interface ITenantBillingRepository Task SaveChangesAsync(CancellationToken cancellationToken = default); /// - /// 管理员端分页查询账单列表(跨租户)。 + /// 系统任务分页查询账单列表(跨租户)。 /// /// 租户 ID 筛选(可选)。 /// 账单状态筛选(可选)。 @@ -147,7 +147,7 @@ public interface ITenantBillingRepository /// /// 获取账单统计数据(用于报表与仪表盘)。 /// - /// 租户 ID(可选,管理员可查询所有租户)。 + /// 租户 ID(可选,系统任务可跨租户统计)。 /// 统计开始时间(UTC)。 /// 统计结束时间(UTC)。 /// 分组方式(Day/Week/Month)。 @@ -161,7 +161,7 @@ public interface ITenantBillingRepository CancellationToken cancellationToken = default); /// - /// 按 ID 获取账单(不限租户,管理员端使用)。 + /// 按 ID 获取账单(不限租户,系统任务使用)。 /// /// 账单 ID。 /// 取消标记。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs index 7d8d382..02284a1 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs @@ -151,7 +151,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena return Array.Empty(); } - // 1. 忽略全局过滤器以支持管理员端跨租户导出/批量操作 + // 1. 忽略全局过滤器以支持系统任务跨租户导出/批量操作 return await context.TenantBillingStatements .IgnoreQueryFilters() .AsNoTracking() @@ -192,7 +192,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena int pageSize, CancellationToken cancellationToken = default) { - // 1. 构建基础查询(管理员端跨租户查询,忽略过滤器) + // 1. 构建基础查询(系统任务跨租户查询,忽略过滤器) var query = context.TenantBillingStatements .IgnoreQueryFilters() .AsNoTracking()