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()