From 3297ff26ab1cfddb74637c758f7b652f612bb97a Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 12:06:52 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E4=BE=A7=E8=83=BD=E5=8A=9B=E5=B9=B6=E6=94=B6=E7=B4=A7?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Commands/ClaimMerchantReviewCommand.cs | 15 - .../Merchants/Commands/ReleaseClaimCommand.cs | 15 - .../Commands/ReviewMerchantCommand.cs | 13 - .../Commands/ReviewMerchantDocumentCommand.cs | 14 - .../Commands/RevokeMerchantReviewCommand.cs | 19 -- .../App/Merchants/Dto/ClaimInfoDto.cs | 37 -- .../Dto/MerchantReviewListItemDto.cs | 69 ---- .../Handlers/ClaimMerchantReviewHandler.cs | 77 ----- .../GetMerchantReviewClaimQueryHandler.cs | 41 --- .../GetPendingReviewListQueryHandler.cs | 59 ---- .../Merchants/Handlers/ReleaseClaimHandler.cs | 64 ---- .../Handlers/ReviewMerchantCommandHandler.cs | 108 ------ .../ReviewMerchantDocumentCommandHandler.cs | 76 ----- .../Handlers/RevokeMerchantReviewHandler.cs | 61 ---- .../Queries/GetMerchantReviewClaimQuery.cs | 9 - .../Queries/GetPendingReviewListQuery.cs | 37 -- .../Validators/ReviewMerchantValidator.cs | 24 -- .../Commands/ApproveStoreCommand.cs | 20 -- .../Commands/ForceCloseStoreCommand.cs | 25 -- .../Commands/RejectStoreCommand.cs | 30 -- .../Commands/ReopenStoreCommand.cs | 20 -- .../StoreAudits/Dto/PendingStoreAuditDto.cs | 84 ----- .../Dto/StoreAuditActionResultDto.cs | 37 -- .../Dto/StoreAuditDailyTrendDto.cs | 27 -- .../StoreAudits/Dto/StoreAuditDetailDto.cs | 34 -- .../StoreAudits/Dto/StoreAuditMerchantDto.cs | 31 -- .../StoreAudits/Dto/StoreAuditRecordDto.cs | 69 ---- .../Dto/StoreAuditStatisticsDto.cs | 37 -- .../App/StoreAudits/Dto/StoreAuditStoreDto.cs | 82 ----- .../StoreAudits/Dto/StoreAuditTenantDto.cs | 31 -- .../Handlers/ApproveStoreCommandHandler.cs | 158 --------- .../Handlers/ForceCloseStoreCommandHandler.cs | 161 --------- .../GetStoreAuditDetailQueryHandler.cs | 322 ------------------ .../GetStoreAuditStatisticsQueryHandler.cs | 279 --------------- .../ListPendingStoreAuditsQueryHandler.cs | 250 -------------- .../ListStoreAuditRecordsQueryHandler.cs | 166 --------- .../Handlers/RejectStoreCommandHandler.cs | 157 --------- .../Handlers/ReopenStoreCommandHandler.cs | 160 --------- .../Queries/GetStoreAuditDetailQuery.cs | 15 - .../Queries/GetStoreAuditStatisticsQuery.cs | 20 -- .../Queries/ListPendingStoreAuditsQuery.cs | 56 --- .../Queries/ListStoreAuditRecordsQuery.cs | 26 -- .../StoreAuditActionNameResolver.cs | 26 -- .../ApproveStoreCommandValidator.cs | 19 -- .../ForceCloseStoreCommandValidator.cs | 20 -- .../Validators/RejectStoreCommandValidator.cs | 21 -- .../Validators/ReopenStoreCommandValidator.cs | 19 -- .../BindInitialTenantSubscriptionCommand.cs | 29 -- .../ChangeTenantSubscriptionPlanCommand.cs | 39 --- .../Commands/ClaimTenantReviewCommand.cs | 17 - .../CreateTenantAdminResetLinkTokenCommand.cs | 15 - .../Commands/CreateTenantBillingCommand.cs | 56 --- .../Commands/CreateTenantManuallyCommand.cs | 264 -------------- .../Commands/CreateTenantPackageCommand.cs | 101 ------ .../CreateTenantSubscriptionCommand.cs | 38 --- .../Commands/DeleteTenantPackageCommand.cs | 14 - .../ExtendTenantSubscriptionCommand.cs | 30 -- .../Commands/ForceClaimTenantReviewCommand.cs | 17 - .../Tenants/Commands/FreezeTenantCommand.cs | 25 -- .../Commands/ImpersonateTenantCommand.cs | 16 - .../Commands/MarkTenantBillingPaidCommand.cs | 30 -- .../Tenants/Commands/RegisterTenantCommand.cs | 71 ---- .../ReleaseTenantReviewClaimCommand.cs | 17 - .../Tenants/Commands/ReviewTenantCommand.cs | 38 --- .../Tenants/Commands/UnfreezeTenantCommand.cs | 24 -- .../Commands/UpdateTenantPackageCommand.cs | 106 ------ .../App/Tenants/Dto/TenantAuditLogDto.cs | 58 ---- .../App/Tenants/Dto/TenantDetailDto.cs | 27 -- .../App/Tenants/Dto/TenantDto.cs | 84 ----- .../App/Tenants/Dto/TenantPackageTenantDto.cs | 50 --- .../App/Tenants/Dto/TenantPackageUsageDto.cs | 52 --- .../App/Tenants/Dto/TenantReviewClaimDto.cs | 38 --- .../App/Tenants/Dto/TenantSubscriptionDto.cs | 54 --- ...InitialTenantSubscriptionCommandHandler.cs | 112 ------ ...ngeTenantSubscriptionPlanCommandHandler.cs | 76 ----- .../ClaimTenantReviewCommandHandler.cs | 92 ----- ...TenantAdminResetLinkTokenCommandHandler.cs | 94 ----- .../CreateTenantAnnouncementCommandHandler.cs | 34 +- .../CreateTenantBillingCommandHandler.cs | 52 --- .../CreateTenantManuallyCommandHandler.cs | 269 --------------- .../CreateTenantPackageCommandHandler.cs | 56 --- .../CreateTenantSubscriptionCommandHandler.cs | 86 ----- .../DeleteTenantAnnouncementCommandHandler.cs | 26 +- .../DeleteTenantPackageCommandHandler.cs | 23 -- .../ExtendTenantSubscriptionCommandHandler.cs | 108 ------ .../ForceClaimTenantReviewCommandHandler.cs | 106 ------ .../Handlers/FreezeTenantCommandHandler.cs | 78 ----- .../GetTenantAuditLogsQueryHandler.cs | 32 -- .../Handlers/GetTenantBillQueryHandler.cs | 26 +- .../Handlers/GetTenantByIdQueryHandler.cs | 43 --- .../GetTenantPackageTenantsQueryHandler.cs | 230 ------------- .../GetTenantPackageUsagesQueryHandler.cs | 153 --------- .../Handlers/GetTenantProgressQueryHandler.cs | 30 +- .../GetTenantQuotaUsageHistoryQueryHandler.cs | 27 +- .../GetTenantReviewClaimQueryHandler.cs | 21 -- .../ImpersonateTenantCommandHandler.cs | 109 ------ .../MarkTenantBillingPaidCommandHandler.cs | 42 --- ...arkTenantNotificationReadCommandHandler.cs | 28 +- .../Handlers/RegisterTenantCommandHandler.cs | 92 ----- .../ReleaseTenantReviewClaimCommandHandler.cs | 67 ---- .../Handlers/ReviewTenantCommandHandler.cs | 226 ------------ .../Handlers/SearchTenantBillsQueryHandler.cs | 28 +- .../SearchTenantNotificationsQueryHandler.cs | 28 +- .../SearchTenantPackagesQueryHandler.cs | 37 -- .../Handlers/SearchTenantsQueryHandler.cs | 59 ---- .../SubmitTenantVerificationCommandHandler.cs | 29 +- .../Handlers/UnfreezeTenantCommandHandler.cs | 76 ----- .../UpdateTenantAnnouncementCommandHandler.cs | 30 +- .../Handlers/UpdateTenantCommandHandler.cs | 35 +- .../UpdateTenantPackageCommandHandler.cs | 65 ---- .../Queries/GetTenantAuditLogsQuery.cs | 13 - .../App/Tenants/Queries/GetTenantByIdQuery.cs | 9 - .../Queries/GetTenantPackageTenantsQuery.cs | 36 -- .../Queries/GetTenantPackageUsagesQuery.cs | 16 - .../Queries/GetTenantReviewClaimQuery.cs | 9 - .../Queries/SearchTenantPackagesQuery.cs | 31 -- .../App/Tenants/Queries/SearchTenantsQuery.cs | 52 --- .../App/Tenants/TenantMapping.cs | 80 ----- .../CreateAnnouncementCommandValidator.cs | 10 +- .../Validators/ReviewTenantValidator.cs | 28 -- .../Validators/SearchTenantsQueryValidator.cs | 23 -- ...CreateAnnouncementCommandValidatorTests.cs | 20 +- .../Tenants/AnnouncementRegressionTests.cs | 3 +- .../App/Tenants/AnnouncementWorkflowTests.cs | 6 +- 124 files changed, 280 insertions(+), 7231 deletions(-) delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ClaimMerchantReviewCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReleaseClaimCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantDocumentCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/RevokeMerchantReviewCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Dto/ClaimInfoDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantReviewListItemDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ClaimMerchantReviewHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantReviewClaimQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetPendingReviewListQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReleaseClaimHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantDocumentCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/RevokeMerchantReviewHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantReviewClaimQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetPendingReviewListQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Validators/ReviewMerchantValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ApproveStoreCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ForceCloseStoreCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/RejectStoreCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ReopenStoreCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/PendingStoreAuditDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditActionResultDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDailyTrendDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDetailDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditMerchantDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditRecordDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStatisticsDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStoreDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditTenantDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ApproveStoreCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ForceCloseStoreCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditDetailQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditStatisticsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListPendingStoreAuditsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListStoreAuditRecordsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/RejectStoreCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ReopenStoreCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditDetailQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditStatisticsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListPendingStoreAuditsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListStoreAuditRecordsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/StoreAuditActionNameResolver.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ApproveStoreCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ForceCloseStoreCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/RejectStoreCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ReopenStoreCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ChangeTenantSubscriptionPlanCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantBillingCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantManuallyCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantSubscriptionCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ExtendTenantSubscriptionCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/FreezeTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantBillingPaidCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RegisterTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReviewTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDetailDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageTenantDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantReviewClaimDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantSubscriptionDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ChangeTenantSubscriptionPlanCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantBillingCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantSubscriptionCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ExtendTenantSubscriptionCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/FreezeTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageTenantsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RegisterTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantPackagesQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageTenantsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageUsagesQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantPackagesQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ClaimMerchantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ClaimMerchantReviewCommand.cs deleted file mode 100644 index 3002b49..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ClaimMerchantReviewCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Dto; - -namespace TakeoutSaaS.Application.App.Merchants.Commands; - -/// -/// 领取商户审核命令。 -/// -public sealed class ClaimMerchantReviewCommand : IRequest -{ - /// - /// 商户 ID。 - /// - public long MerchantId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReleaseClaimCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReleaseClaimCommand.cs deleted file mode 100644 index 5a06141..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReleaseClaimCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Dto; - -namespace TakeoutSaaS.Application.App.Merchants.Commands; - -/// -/// 释放商户审核领取命令。 -/// -public sealed class ReleaseClaimCommand : IRequest -{ - /// - /// 商户 ID。 - /// - public long MerchantId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantCommand.cs deleted file mode 100644 index 00236ba..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantCommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Merchants.Dto; - -namespace TakeoutSaaS.Application.App.Merchants.Commands; - -/// -/// 审核商户入驻。 -/// -public sealed record ReviewMerchantCommand( - [param: Required] long MerchantId, - bool Approve, - string? Remarks) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantDocumentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantDocumentCommand.cs deleted file mode 100644 index 0e01c26..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantDocumentCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Merchants.Dto; - -namespace TakeoutSaaS.Application.App.Merchants.Commands; - -/// -/// 审核商户证照。 -/// -public sealed record ReviewMerchantDocumentCommand( - [property: Required] long MerchantId, - [property: Required] long DocumentId, - bool Approve, - string? Remarks) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/RevokeMerchantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/RevokeMerchantReviewCommand.cs deleted file mode 100644 index 30580f4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/RevokeMerchantReviewCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Merchants.Commands; - -/// -/// 撤销商户审核命令。 -/// -public sealed class RevokeMerchantReviewCommand : IRequest -{ - /// - /// 商户 ID。 - /// - public long MerchantId { get; init; } - - /// - /// 撤销原因。 - /// - public string Reason { get; init; } = string.Empty; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/ClaimInfoDto.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/ClaimInfoDto.cs deleted file mode 100644 index 999aead..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/ClaimInfoDto.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Merchants.Dto; - -/// -/// 审核领取信息 DTO。 -/// -public sealed class ClaimInfoDto -{ - /// - /// 商户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long MerchantId { get; init; } - - /// - /// 领取人 ID。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? ClaimedBy { get; init; } - - /// - /// 领取人名称。 - /// - public string? ClaimedByName { get; init; } - - /// - /// 领取时间。 - /// - public DateTime? ClaimedAt { get; init; } - - /// - /// 领取过期时间。 - /// - public DateTime? ClaimExpiresAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantReviewListItemDto.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantReviewListItemDto.cs deleted file mode 100644 index 2775ff8..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantReviewListItemDto.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Common.Enums; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Merchants.Dto; - -/// -/// 待审核商户列表项 DTO。 -/// -public sealed class MerchantReviewListItemDto -{ - /// - /// 商户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string? TenantName { get; init; } - - /// - /// 商户名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 经营模式。 - /// - public OperatingMode? OperatingMode { get; init; } - - /// - /// 营业执照号。 - /// - public string? LicenseNumber { get; init; } - - /// - /// 审核状态。 - /// - public MerchantStatus Status { get; init; } - - /// - /// 领取人名称。 - /// - public string? ClaimedByName { get; init; } - - /// - /// 领取时间。 - /// - public DateTime? ClaimedAt { get; init; } - - /// - /// 领取过期时间。 - /// - public DateTime? ClaimExpiresAt { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ClaimMerchantReviewHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ClaimMerchantReviewHandler.cs deleted file mode 100644 index fdd10a0..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ClaimMerchantReviewHandler.cs +++ /dev/null @@ -1,77 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Commands; -using TakeoutSaaS.Application.App.Merchants.Dto; -using TakeoutSaaS.Domain.Merchants.Entities; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Merchants.Handlers; - -/// -/// 领取商户审核处理器。 -/// -public sealed class ClaimMerchantReviewHandler( - IMerchantRepository merchantRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(ClaimMerchantReviewCommand request, CancellationToken cancellationToken) - { - var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); - - if (merchant.Status != MerchantStatus.Pending) - { - throw new BusinessException(ErrorCodes.Conflict, "商户不在待审核状态"); - } - - var now = DateTime.UtcNow; - if (merchant.ClaimedBy.HasValue && merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt > now) - { - if (merchant.ClaimedBy != currentUserAccessor.UserId) - { - throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取"); - } - - return ToDto(merchant); - } - - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - merchant.ClaimedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId; - merchant.ClaimedByName = actorName; - merchant.ClaimedAt = now; - merchant.ClaimExpiresAt = now.AddMinutes(30); - - await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken); - await merchantRepository.AddAuditLogAsync(new MerchantAuditLog - { - TenantId = merchant.TenantId, - MerchantId = merchant.Id, - Action = MerchantAuditAction.ReviewClaimed, - Title = "领取审核", - Description = $"领取人:{actorName}", - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName - }, cancellationToken); - await merchantRepository.SaveChangesAsync(cancellationToken); - - return ToDto(merchant); - } - - private static ClaimInfoDto ToDto(Domain.Merchants.Entities.Merchant merchant) - => new() - { - MerchantId = merchant.Id, - ClaimedBy = merchant.ClaimedBy, - ClaimedByName = merchant.ClaimedByName, - ClaimedAt = merchant.ClaimedAt, - ClaimExpiresAt = merchant.ClaimExpiresAt - }; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantReviewClaimQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantReviewClaimQueryHandler.cs deleted file mode 100644 index 5578635..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantReviewClaimQueryHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Dto; -using TakeoutSaaS.Application.App.Merchants.Queries; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Merchants.Handlers; - -/// -/// 商户审核领取信息查询处理器。 -/// -public sealed class GetMerchantReviewClaimQueryHandler(IMerchantRepository merchantRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetMerchantReviewClaimQuery request, CancellationToken cancellationToken) - { - var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); - - if (!merchant.ClaimedBy.HasValue) - { - return null; - } - - if (merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt <= DateTime.UtcNow) - { - return null; - } - - return new ClaimInfoDto - { - MerchantId = merchant.Id, - ClaimedBy = merchant.ClaimedBy, - ClaimedByName = merchant.ClaimedByName, - ClaimedAt = merchant.ClaimedAt, - ClaimExpiresAt = merchant.ClaimExpiresAt - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetPendingReviewListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetPendingReviewListQueryHandler.cs deleted file mode 100644 index d570d71..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetPendingReviewListQueryHandler.cs +++ /dev/null @@ -1,59 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Dto; -using TakeoutSaaS.Application.App.Merchants.Queries; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Merchants.Handlers; - -/// -/// 待审核商户列表处理器。 -/// -public sealed class GetPendingReviewListQueryHandler( - IMerchantRepository merchantRepository, - ITenantRepository tenantRepository) - : IRequestHandler> -{ - /// - public async Task> Handle( - GetPendingReviewListQuery request, - CancellationToken cancellationToken) - { - var merchants = await merchantRepository.SearchAsync( - request.TenantId, - MerchantStatus.Pending, - request.OperatingMode, - request.Keyword, - cancellationToken); - - var total = merchants.Count; - var paged = merchants - .OrderByDescending(x => x.CreatedAt) - .Skip((request.Page - 1) * request.PageSize) - .Take(request.PageSize) - .ToList(); - - var tenantIds = paged.Select(x => x.TenantId).Distinct().ToArray(); - var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken); - var tenantLookup = tenants.ToDictionary(x => x.Id, x => x.Name); - - var items = paged.Select(merchant => new MerchantReviewListItemDto - { - Id = merchant.Id, - TenantId = merchant.TenantId, - TenantName = tenantLookup.TryGetValue(merchant.TenantId, out var name) ? name : null, - Name = merchant.BrandName, - OperatingMode = merchant.OperatingMode, - LicenseNumber = merchant.BusinessLicenseNumber, - Status = merchant.Status, - ClaimedByName = merchant.ClaimedByName, - ClaimedAt = merchant.ClaimedAt, - ClaimExpiresAt = merchant.ClaimExpiresAt, - CreatedAt = merchant.CreatedAt - }).ToList(); - - return new PagedResult(items, request.Page, request.PageSize, total); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReleaseClaimHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReleaseClaimHandler.cs deleted file mode 100644 index a9df182..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReleaseClaimHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Commands; -using TakeoutSaaS.Application.App.Merchants.Dto; -using TakeoutSaaS.Domain.Merchants.Entities; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Merchants.Handlers; - -/// -/// 释放审核领取处理器。 -/// -public sealed class ReleaseClaimHandler( - IMerchantRepository merchantRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(ReleaseClaimCommand request, CancellationToken cancellationToken) - { - var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); - - if (!merchant.ClaimedBy.HasValue) - { - return null; - } - - var now = DateTime.UtcNow; - var claimExpired = merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt <= now; - - if (!claimExpired && merchant.ClaimedBy != currentUserAccessor.UserId) - { - throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取"); - } - - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - merchant.ClaimedBy = null; - merchant.ClaimedByName = null; - merchant.ClaimedAt = null; - merchant.ClaimExpiresAt = null; - - await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken); - await merchantRepository.AddAuditLogAsync(new MerchantAuditLog - { - TenantId = merchant.TenantId, - MerchantId = merchant.Id, - Action = MerchantAuditAction.ReviewReleased, - Title = "释放审核", - Description = $"释放人:{actorName}", - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName - }, cancellationToken); - await merchantRepository.SaveChangesAsync(cancellationToken); - - return null; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantCommandHandler.cs deleted file mode 100644 index 20ddefa..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantCommandHandler.cs +++ /dev/null @@ -1,108 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Commands; -using TakeoutSaaS.Application.App.Merchants.Dto; -using TakeoutSaaS.Domain.Merchants.Entities; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Merchants.Handlers; - -/// -/// 商户审核处理器。 -/// -public sealed class ReviewMerchantCommandHandler( - IMerchantRepository merchantRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - /// 审核商户。 - /// - /// 审核命令。 - /// 取消标记。 - /// 商户 DTO。 - public async Task Handle(ReviewMerchantCommand request, CancellationToken cancellationToken) - { - // 1. 读取商户 - var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); - - var now = DateTime.UtcNow; - if (!merchant.ClaimedBy.HasValue || !merchant.ClaimExpiresAt.HasValue || merchant.ClaimExpiresAt <= now) - { - throw new BusinessException(ErrorCodes.Conflict, "请先领取审核"); - } - - if (merchant.ClaimedBy != currentUserAccessor.UserId) - { - throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取"); - } - - if (merchant.Status != MerchantStatus.Pending) - { - throw new BusinessException(ErrorCodes.Conflict, "商户不在待审核状态"); - } - - // 2. 更新审核状态 - merchant.Status = request.Approve ? MerchantStatus.Approved : MerchantStatus.Rejected; - merchant.ReviewRemarks = request.Remarks; - merchant.LastReviewedAt = now; - merchant.LastReviewedBy = ResolveOperatorId(); - if (request.Approve && merchant.JoinedAt == null) - { - merchant.JoinedAt = now; - } - - if (request.Approve) - { - merchant.IsFrozen = false; - merchant.FrozenReason = null; - merchant.FrozenAt = null; - merchant.ApprovedAt = now; - merchant.ApprovedBy = ResolveOperatorId(); - } - else - { - merchant.IsFrozen = false; - merchant.FrozenReason = null; - merchant.FrozenAt = null; - } - - merchant.ClaimedBy = null; - merchant.ClaimedByName = null; - merchant.ClaimedAt = null; - merchant.ClaimExpiresAt = null; - - // 3. 持久化与审计 - await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken); - await merchantRepository.AddAuditLogAsync(new MerchantAuditLog - { - TenantId = merchant.TenantId, - MerchantId = merchant.Id, - Action = request.Approve ? MerchantAuditAction.ReviewApproved : MerchantAuditAction.ReviewRejected, - Title = request.Approve ? "商户审核通过" : "商户审核驳回", - Description = request.Remarks, - OperatorId = ResolveOperatorId(), - OperatorName = ResolveOperatorName() - }, cancellationToken); - await merchantRepository.SaveChangesAsync(cancellationToken); - - // 4. 返回 DTO - return MerchantMapping.ToDto(merchant); - } - - private long? ResolveOperatorId() - { - var id = currentUserAccessor.UserId; - return id == 0 ? null : id; - } - - private string ResolveOperatorName() - { - var id = currentUserAccessor.UserId; - return id == 0 ? "system" : $"user:{id}"; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantDocumentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantDocumentCommandHandler.cs deleted file mode 100644 index a8a037f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantDocumentCommandHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Commands; -using TakeoutSaaS.Application.App.Merchants.Dto; -using TakeoutSaaS.Domain.Merchants.Entities; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Merchants.Handlers; - -/// -/// 审核证照处理器。 -/// -public sealed class ReviewMerchantDocumentCommandHandler( - IMerchantRepository merchantRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - /// 审核商户证照。 - /// - /// 审核命令。 - /// 取消标记。 - /// 证照 DTO。 - public async Task Handle(ReviewMerchantDocumentCommand request, CancellationToken cancellationToken) - { - // 1. 读取证照 - var tenantId = tenantProvider.GetCurrentTenantId(); - var document = await merchantRepository.FindDocumentByIdAsync(request.MerchantId, tenantId, request.DocumentId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "证照不存在"); - - // 2. 若状态无变化且备注相同,直接返回 - var targetStatus = request.Approve ? MerchantDocumentStatus.Approved : MerchantDocumentStatus.Rejected; - if (document.Status == targetStatus && document.Remarks == request.Remarks) - { - return MerchantMapping.ToDto(document); - } - - // 3. 更新状态 - document.Status = targetStatus; - document.Remarks = request.Remarks; - - // 4. 持久化与审计 - await merchantRepository.UpdateDocumentAsync(document, cancellationToken); - await merchantRepository.AddAuditLogAsync(new MerchantAuditLog - { - TenantId = tenantId, - MerchantId = document.MerchantId, - Action = MerchantAuditAction.DocumentReviewed, - Title = request.Approve ? "证照审核通过" : "证照审核驳回", - Description = request.Remarks, - OperatorId = ResolveOperatorId(), - OperatorName = ResolveOperatorName() - }, cancellationToken); - await merchantRepository.SaveChangesAsync(cancellationToken); - - // 5. 返回 DTO - return MerchantMapping.ToDto(document); - } - - private long? ResolveOperatorId() - { - var id = currentUserAccessor.UserId; - return id == 0 ? null : id; - } - - private string ResolveOperatorName() - { - var id = currentUserAccessor.UserId; - return id == 0 ? "system" : $"user:{id}"; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/RevokeMerchantReviewHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/RevokeMerchantReviewHandler.cs deleted file mode 100644 index deb76cf..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/RevokeMerchantReviewHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Commands; -using TakeoutSaaS.Domain.Merchants.Entities; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Merchants.Handlers; - -/// -/// 撤销商户审核处理器。 -/// -public sealed class RevokeMerchantReviewHandler( - IMerchantRepository merchantRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(RevokeMerchantReviewCommand request, CancellationToken cancellationToken) - { - var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); - - if (merchant.Status != MerchantStatus.Approved) - { - throw new BusinessException(ErrorCodes.Conflict, "商户不在已审核状态"); - } - - var now = DateTime.UtcNow; - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - merchant.Status = MerchantStatus.Pending; - merchant.IsFrozen = true; - merchant.FrozenReason = request.Reason; - merchant.FrozenAt = now; - merchant.ReviewRemarks = request.Reason; - merchant.LastReviewedAt = now; - merchant.LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId; - merchant.ClaimedBy = null; - merchant.ClaimedByName = null; - merchant.ClaimedAt = null; - merchant.ClaimExpiresAt = null; - - await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken); - await merchantRepository.AddAuditLogAsync(new MerchantAuditLog - { - TenantId = merchant.TenantId, - MerchantId = merchant.Id, - Action = MerchantAuditAction.ReviewRevoked, - Title = "撤销审核", - Description = request.Reason, - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName - }, cancellationToken); - await merchantRepository.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantReviewClaimQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantReviewClaimQuery.cs deleted file mode 100644 index e829e3e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantReviewClaimQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Dto; - -namespace TakeoutSaaS.Application.App.Merchants.Queries; - -/// -/// 获取商户审核领取信息查询。 -/// -public sealed record GetMerchantReviewClaimQuery(long MerchantId) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetPendingReviewListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetPendingReviewListQuery.cs deleted file mode 100644 index 3300be0..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetPendingReviewListQuery.cs +++ /dev/null @@ -1,37 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Merchants.Dto; -using TakeoutSaaS.Domain.Common.Enums; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Merchants.Queries; - -/// -/// 待审核商户列表查询。 -/// -public sealed class GetPendingReviewListQuery : IRequest> -{ - /// - /// 关键词(商户名称/营业执照号)。 - /// - public string? Keyword { get; init; } - - /// - /// 经营模式筛选。 - /// - public OperatingMode? OperatingMode { get; init; } - - /// - /// 租户筛选。 - /// - public long? TenantId { get; init; } - - /// - /// 页码。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/ReviewMerchantValidator.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/ReviewMerchantValidator.cs deleted file mode 100644 index 69ab1c7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/ReviewMerchantValidator.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Merchants.Commands; - -namespace TakeoutSaaS.Application.App.Merchants.Validators; - -/// -/// 商户审核命令验证器。 -/// -public sealed class ReviewMerchantValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public ReviewMerchantValidator() - { - RuleFor(x => x.MerchantId).GreaterThan(0); - RuleFor(x => x.Remarks) - .NotEmpty() - .When(x => !x.Approve); - RuleFor(x => x.Remarks) - .MaximumLength(500) - .When(x => !string.IsNullOrWhiteSpace(x.Remarks)); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ApproveStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ApproveStoreCommand.cs deleted file mode 100644 index 882d9e0..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ApproveStoreCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; - -namespace TakeoutSaaS.Application.App.StoreAudits.Commands; - -/// -/// 审核通过命令。 -/// -public sealed record ApproveStoreCommand : IRequest -{ - /// - /// 门店 ID。 - /// - public long StoreId { get; init; } - - /// - /// 审核备注。 - /// - public string? Remark { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ForceCloseStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ForceCloseStoreCommand.cs deleted file mode 100644 index 14cf24a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ForceCloseStoreCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; - -namespace TakeoutSaaS.Application.App.StoreAudits.Commands; - -/// -/// 强制关闭门店命令。 -/// -public sealed record ForceCloseStoreCommand : IRequest -{ - /// - /// 门店 ID。 - /// - public long StoreId { get; init; } - - /// - /// 关闭原因。 - /// - public string Reason { get; init; } = string.Empty; - - /// - /// 备注。 - /// - public string? Remark { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/RejectStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/RejectStoreCommand.cs deleted file mode 100644 index 1f0f238..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/RejectStoreCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; - -namespace TakeoutSaaS.Application.App.StoreAudits.Commands; - -/// -/// 审核驳回命令。 -/// -public sealed record RejectStoreCommand : IRequest -{ - /// - /// 门店 ID。 - /// - public long StoreId { get; init; } - - /// - /// 驳回原因 ID。 - /// - public long RejectionReasonId { get; init; } - - /// - /// 驳回原因补充说明。 - /// - public string? RejectionReasonText { get; init; } - - /// - /// 审核备注。 - /// - public string? Remark { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ReopenStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ReopenStoreCommand.cs deleted file mode 100644 index 74755ff..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ReopenStoreCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; - -namespace TakeoutSaaS.Application.App.StoreAudits.Commands; - -/// -/// 解除强制关闭命令。 -/// -public sealed record ReopenStoreCommand : IRequest -{ - /// - /// 门店 ID。 - /// - public long StoreId { get; init; } - - /// - /// 备注。 - /// - public string? Remark { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/PendingStoreAuditDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/PendingStoreAuditDto.cs deleted file mode 100644 index 3087fd5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/PendingStoreAuditDto.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 待审核门店 DTO。 -/// -public sealed record PendingStoreAuditDto -{ - /// - /// 门店 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long StoreId { get; init; } - - /// - /// 门店名称。 - /// - public string StoreName { get; init; } = string.Empty; - - /// - /// 门店编码。 - /// - public string StoreCode { get; init; } = string.Empty; - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 商户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long MerchantId { get; init; } - - /// - /// 商户名称。 - /// - public string MerchantName { get; init; } = string.Empty; - - /// - /// 门头招牌图。 - /// - public string? SignboardImageUrl { get; init; } - - /// - /// 完整地址。 - /// - public string FullAddress { get; init; } = string.Empty; - - /// - /// 主体类型。 - /// - public StoreOwnershipType OwnershipType { get; init; } - - /// - /// 提交时间。 - /// - public DateTime? SubmittedAt { get; init; } - - /// - /// 等待天数。 - /// - public int WaitingDays { get; init; } - - /// - /// 是否超时。 - /// - public bool IsOverdue { get; init; } - - /// - /// 资质数量。 - /// - public int QualificationCount { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditActionResultDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditActionResultDto.cs deleted file mode 100644 index d99b10b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditActionResultDto.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 审核/风控操作结果 DTO。 -/// -public sealed record StoreAuditActionResultDto -{ - /// - /// 门店 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long StoreId { get; init; } - - /// - /// 审核状态。 - /// - public StoreAuditStatus AuditStatus { get; init; } - - /// - /// 经营状态。 - /// - public StoreBusinessStatus BusinessStatus { get; init; } - - /// - /// 驳回原因。 - /// - public string? RejectionReason { get; init; } - - /// - /// 提示信息。 - /// - public string? Message { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDailyTrendDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDailyTrendDto.cs deleted file mode 100644 index a4d84f3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDailyTrendDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 审核统计趋势项。 -/// -public sealed record StoreAuditDailyTrendDto -{ - /// - /// 日期。 - /// - public DateOnly Date { get; init; } - - /// - /// 提交数量。 - /// - public int Submitted { get; init; } - - /// - /// 通过数量。 - /// - public int Approved { get; init; } - - /// - /// 驳回数量。 - /// - public int Rejected { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDetailDto.cs deleted file mode 100644 index 3530167..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDetailDto.cs +++ /dev/null @@ -1,34 +0,0 @@ -using TakeoutSaaS.Application.App.Stores.Dto; - -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 门店审核详情 DTO。 -/// -public sealed record StoreAuditDetailDto -{ - /// - /// 门店信息。 - /// - public StoreAuditStoreDto Store { get; init; } = new(); - - /// - /// 租户信息。 - /// - public StoreAuditTenantDto Tenant { get; init; } = new(); - - /// - /// 商户信息。 - /// - public StoreAuditMerchantDto Merchant { get; init; } = new(); - - /// - /// 资质列表。 - /// - public IReadOnlyList Qualifications { get; init; } = []; - - /// - /// 审核记录。 - /// - public IReadOnlyList AuditHistory { get; init; } = []; -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditMerchantDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditMerchantDto.cs deleted file mode 100644 index 5dcbc6b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditMerchantDto.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 门店审核详情 - 商户信息。 -/// -public sealed record StoreAuditMerchantDto -{ - /// - /// 商户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 商户名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 法人或主体名称。 - /// - public string? LegalName { get; init; } - - /// - /// 统一社会信用代码。 - /// - public string? CreditCode { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditRecordDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditRecordDto.cs deleted file mode 100644 index 15b8993..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditRecordDto.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 门店审核记录 DTO。 -/// -public sealed record StoreAuditRecordDto -{ - /// - /// 记录 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 审核动作。 - /// - public StoreAuditAction Action { get; init; } - - /// - /// 动作名称。 - /// - public string ActionName { get; init; } = string.Empty; - - /// - /// 操作人 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long? OperatorId { get; init; } - - /// - /// 操作人名称。 - /// - public string OperatorName { get; init; } = string.Empty; - - /// - /// 操作前状态。 - /// - public StoreAuditStatus? PreviousStatus { get; init; } - - /// - /// 操作后状态。 - /// - public StoreAuditStatus NewStatus { get; init; } - - /// - /// 驳回理由 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long? RejectionReasonId { get; init; } - - /// - /// 驳回理由。 - /// - public string? RejectionReasonText { get; init; } - - /// - /// 备注。 - /// - public string? Remark { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStatisticsDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStatisticsDto.cs deleted file mode 100644 index 495b10f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStatisticsDto.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 审核统计 DTO。 -/// -public sealed record StoreAuditStatisticsDto -{ - /// - /// 待审核数量。 - /// - public int PendingCount { get; init; } - - /// - /// 超时数量。 - /// - public int OverdueCount { get; init; } - - /// - /// 审核通过数量。 - /// - public int ApprovedCount { get; init; } - - /// - /// 审核驳回数量。 - /// - public int RejectedCount { get; init; } - - /// - /// 平均处理时长(小时)。 - /// - public double AvgProcessingHours { get; init; } - - /// - /// 每日趋势。 - /// - public IReadOnlyList DailyTrend { get; init; } = []; -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStoreDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStoreDto.cs deleted file mode 100644 index 110638e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStoreDto.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 门店审核详情 - 门店信息。 -/// -public sealed record StoreAuditStoreDto -{ - /// - /// 门店 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 门店名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 门店编码。 - /// - public string Code { get; init; } = string.Empty; - - /// - /// 联系电话。 - /// - public string? Phone { get; init; } - - /// - /// 门头招牌图。 - /// - public string? SignboardImageUrl { get; init; } - - /// - /// 省份。 - /// - public string? Province { get; init; } - - /// - /// 城市。 - /// - public string? City { get; init; } - - /// - /// 区县。 - /// - public string? District { get; init; } - - /// - /// 详细地址。 - /// - public string? Address { get; init; } - - /// - /// 经度。 - /// - public double? Longitude { get; init; } - - /// - /// 纬度。 - /// - public double? Latitude { get; init; } - - /// - /// 主体类型。 - /// - public StoreOwnershipType OwnershipType { get; init; } - - /// - /// 审核状态。 - /// - public StoreAuditStatus AuditStatus { get; init; } - - /// - /// 提交时间。 - /// - public DateTime? SubmittedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditTenantDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditTenantDto.cs deleted file mode 100644 index 50d3cfd..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditTenantDto.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.StoreAudits.Dto; - -/// -/// 门店审核详情 - 租户信息。 -/// -public sealed record StoreAuditTenantDto -{ - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 联系人。 - /// - public string? ContactName { get; init; } - - /// - /// 联系电话。 - /// - public string? ContactPhone { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ApproveStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ApproveStoreCommandHandler.cs deleted file mode 100644 index a08d565..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ApproveStoreCommandHandler.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.StoreAudits.Commands; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Domain.Stores.Entities; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 审核通过处理器。 -/// -public sealed class ApproveStoreCommandHandler( - IStoreRepository storeRepository, - IDapperExecutor dapperExecutor, - ICurrentUserAccessor currentUserAccessor, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(ApproveStoreCommand request, CancellationToken cancellationToken) - { - // 1. 获取门店快照 - var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken); - if (!snapshot.HasValue) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 1.1 (空行后) 校验审核状态 - if (snapshot.Value.AuditStatus != StoreAuditStatus.Pending) - { - throw new BusinessException(ErrorCodes.Conflict, "门店不处于待审核状态"); - } - - // 2. (空行后) 获取门店实体 - var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken); - if (store is null) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 3. (空行后) 更新状态并记录审核 - var previousStatus = store.AuditStatus; - var now = DateTime.UtcNow; - store.AuditStatus = StoreAuditStatus.Activated; - store.BusinessStatus = StoreBusinessStatus.Resting; - store.ActivatedAt ??= now; - store.RejectionReason = null; - store.ClosureReason = null; - store.ClosureReasonText = null; - - await storeRepository.UpdateStoreAsync(store, cancellationToken); - await storeRepository.AddAuditRecordAsync(new StoreAuditRecord - { - StoreId = store.Id, - Action = StoreAuditAction.Approve, - PreviousStatus = previousStatus, - NewStatus = store.AuditStatus, - OperatorId = ResolveOperatorId(), - OperatorName = ResolveOperatorName(), - Remarks = request.Remark - }, cancellationToken); - await storeRepository.SaveChangesAsync(cancellationToken); - logger.LogInformation("门店 {StoreId} 审核通过", store.Id); - - // 4. (空行后) 返回结果 - return new StoreAuditActionResultDto - { - StoreId = store.Id, - AuditStatus = store.AuditStatus, - BusinessStatus = store.BusinessStatus, - Message = "门店已激活" - }; - } - - private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync( - long storeId, - CancellationToken cancellationToken) - { - // 1. 查询门店基础字段 - return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - await using var command = CreateCommand( - connection, - BuildStoreSnapshotSql(), - [ - ("storeId", storeId) - ]); - - // 1.1 (空行后) 执行查询 - await using var reader = await command.ExecuteReaderAsync(token); - if (!await reader.ReadAsync(token)) - { - return null; - } - - // 1.2 (空行后) 返回快照 - return ( - reader.GetInt64(reader.GetOrdinal("TenantId")), - (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")), - (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus"))); - }, - cancellationToken); - } - - private long? ResolveOperatorId() - { - var id = currentUserAccessor.UserId; - return id == 0 ? null : id; - } - - private string ResolveOperatorName() - { - var id = currentUserAccessor.UserId; - return id == 0 ? "system" : $"user:{id}"; - } - - private static string BuildStoreSnapshotSql() - { - return """ - select - s."TenantId", - s."AuditStatus", - s."BusinessStatus" - from public.stores s - where s."DeletedAt" is null - and s."Id" = @storeId; - """; - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ForceCloseStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ForceCloseStoreCommandHandler.cs deleted file mode 100644 index 8adbdac..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ForceCloseStoreCommandHandler.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.StoreAudits.Commands; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Domain.Stores.Entities; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 强制关闭门店处理器。 -/// -public sealed class ForceCloseStoreCommandHandler( - IStoreRepository storeRepository, - IDapperExecutor dapperExecutor, - ICurrentUserAccessor currentUserAccessor, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(ForceCloseStoreCommand request, CancellationToken cancellationToken) - { - // 1. 获取门店快照 - var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken); - if (!snapshot.HasValue) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 1.1 (空行后) 校验审核与经营状态 - if (snapshot.Value.AuditStatus != StoreAuditStatus.Activated) - { - throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法强制关闭"); - } - - if (snapshot.Value.BusinessStatus == StoreBusinessStatus.ForceClosed) - { - throw new BusinessException(ErrorCodes.Conflict, "门店已处于强制关闭状态"); - } - - // 2. (空行后) 获取门店实体 - var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken); - if (store is null) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 3. (空行后) 更新状态并记录风控 - var now = DateTime.UtcNow; - store.BusinessStatus = StoreBusinessStatus.ForceClosed; - store.ClosureReason = StoreClosureReason.PlatformSuspended; - store.ClosureReasonText = request.Reason; - store.ForceCloseReason = request.Reason; - store.ForceClosedAt = now; - - await storeRepository.UpdateStoreAsync(store, cancellationToken); - await storeRepository.AddAuditRecordAsync(new StoreAuditRecord - { - StoreId = store.Id, - Action = StoreAuditAction.ForceClose, - PreviousStatus = store.AuditStatus, - NewStatus = store.AuditStatus, - OperatorId = ResolveOperatorId(), - OperatorName = ResolveOperatorName(), - Remarks = request.Remark - }, cancellationToken); - await storeRepository.SaveChangesAsync(cancellationToken); - logger.LogInformation("门店 {StoreId} 强制关闭", store.Id); - - // 4. (空行后) 返回结果 - return new StoreAuditActionResultDto - { - StoreId = store.Id, - AuditStatus = store.AuditStatus, - BusinessStatus = store.BusinessStatus, - Message = "门店已强制关闭" - }; - } - - private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync( - long storeId, - CancellationToken cancellationToken) - { - // 1. 查询门店基础字段 - return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - await using var command = CreateCommand( - connection, - BuildStoreSnapshotSql(), - [ - ("storeId", storeId) - ]); - - // 1.1 (空行后) 执行查询 - await using var reader = await command.ExecuteReaderAsync(token); - if (!await reader.ReadAsync(token)) - { - return null; - } - - // 1.2 (空行后) 返回快照 - return ( - reader.GetInt64(reader.GetOrdinal("TenantId")), - (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")), - (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus"))); - }, - cancellationToken); - } - - private long? ResolveOperatorId() - { - var id = currentUserAccessor.UserId; - return id == 0 ? null : id; - } - - private string ResolveOperatorName() - { - var id = currentUserAccessor.UserId; - return id == 0 ? "system" : $"user:{id}"; - } - - private static string BuildStoreSnapshotSql() - { - return """ - select - s."TenantId", - s."AuditStatus", - s."BusinessStatus" - from public.stores s - where s."DeletedAt" is null - and s."Id" = @storeId; - """; - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditDetailQueryHandler.cs deleted file mode 100644 index 6d2a1ca..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditDetailQueryHandler.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Application.App.StoreAudits.Queries; -using TakeoutSaaS.Application.App.Stores.Dto; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 门店审核详情查询处理器。 -/// -public sealed class GetStoreAuditDetailQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler -{ - /// - public async Task Handle(GetStoreAuditDetailQuery request, CancellationToken cancellationToken) - { - // 1. 查询门店与主体信息 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 1.1 查询门店基础信息 - await using var storeCommand = CreateCommand( - connection, - BuildStoreSql(), - [ - ("storeId", request.StoreId) - ]); - - await using var reader = await storeCommand.ExecuteReaderAsync(token); - if (!await reader.ReadAsync(token)) - { - return null; - } - - // 1.2 (空行后) 映射门店信息 - var store = new StoreAuditStoreDto - { - Id = reader.GetInt64(reader.GetOrdinal("StoreId")), - Name = reader.GetString(reader.GetOrdinal("StoreName")), - Code = reader.GetString(reader.GetOrdinal("StoreCode")), - Phone = reader.IsDBNull(reader.GetOrdinal("Phone")) ? null : reader.GetString(reader.GetOrdinal("Phone")), - SignboardImageUrl = reader.IsDBNull(reader.GetOrdinal("SignboardImageUrl")) - ? null - : reader.GetString(reader.GetOrdinal("SignboardImageUrl")), - Province = reader.IsDBNull(reader.GetOrdinal("Province")) ? null : reader.GetString(reader.GetOrdinal("Province")), - City = reader.IsDBNull(reader.GetOrdinal("City")) ? null : reader.GetString(reader.GetOrdinal("City")), - District = reader.IsDBNull(reader.GetOrdinal("District")) ? null : reader.GetString(reader.GetOrdinal("District")), - Address = reader.IsDBNull(reader.GetOrdinal("Address")) ? null : reader.GetString(reader.GetOrdinal("Address")), - Longitude = reader.IsDBNull(reader.GetOrdinal("Longitude")) ? null : reader.GetDouble(reader.GetOrdinal("Longitude")), - Latitude = reader.IsDBNull(reader.GetOrdinal("Latitude")) ? null : reader.GetDouble(reader.GetOrdinal("Latitude")), - OwnershipType = (StoreOwnershipType)reader.GetInt32(reader.GetOrdinal("OwnershipType")), - AuditStatus = (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")), - SubmittedAt = reader.IsDBNull(reader.GetOrdinal("SubmittedAt")) - ? null - : reader.GetDateTime(reader.GetOrdinal("SubmittedAt")) - }; - - // 1.3 (空行后) 映射租户信息 - var tenant = new StoreAuditTenantDto - { - Id = reader.GetInt64(reader.GetOrdinal("TenantId")), - Name = reader.GetString(reader.GetOrdinal("TenantName")), - ContactName = reader.IsDBNull(reader.GetOrdinal("TenantContactName")) - ? null - : reader.GetString(reader.GetOrdinal("TenantContactName")), - ContactPhone = reader.IsDBNull(reader.GetOrdinal("TenantContactPhone")) - ? null - : reader.GetString(reader.GetOrdinal("TenantContactPhone")) - }; - - // 1.4 (空行后) 映射商户信息 - var merchant = new StoreAuditMerchantDto - { - Id = reader.GetInt64(reader.GetOrdinal("MerchantId")), - Name = reader.GetString(reader.GetOrdinal("MerchantName")), - LegalName = reader.IsDBNull(reader.GetOrdinal("MerchantLegalName")) - ? null - : reader.GetString(reader.GetOrdinal("MerchantLegalName")), - CreditCode = reader.IsDBNull(reader.GetOrdinal("MerchantCreditCode")) - ? null - : reader.GetString(reader.GetOrdinal("MerchantCreditCode")) - }; - - // 2. (空行后) 查询资质列表 - var qualifications = await QueryQualificationsAsync(connection, request.StoreId, token); - - // 3. (空行后) 查询审核记录 - var auditHistory = await QueryAuditRecordsAsync(connection, request.StoreId, token); - - // 4. (空行后) 组装结果 - return new StoreAuditDetailDto - { - Store = store, - Tenant = tenant, - Merchant = merchant, - Qualifications = qualifications, - AuditHistory = auditHistory - }; - }, - cancellationToken); - } - - private static async Task> QueryQualificationsAsync( - IDbConnection connection, - long storeId, - CancellationToken cancellationToken) - { - // 1. 查询门店资质 - await using var command = CreateCommand( - connection, - BuildQualificationSql(), - [ - ("storeId", storeId) - ]); - - await using var reader = await command.ExecuteReaderAsync(cancellationToken); - var items = new List(); - if (!reader.HasRows) - { - return items; - } - - // 2. (空行后) 映射资质 DTO - var today = DateOnly.FromDateTime(DateTime.UtcNow); - while (await reader.ReadAsync(cancellationToken)) - { - DateOnly? expiresAt = reader.IsDBNull(reader.GetOrdinal("ExpiresAt")) - ? null - : DateOnly.FromDateTime(reader.GetDateTime(reader.GetOrdinal("ExpiresAt"))); - int? daysUntilExpiry = expiresAt.HasValue - ? expiresAt.Value.DayNumber - today.DayNumber - : null; - var isExpired = expiresAt.HasValue && expiresAt.Value < today; - var isExpiringSoon = expiresAt.HasValue - && expiresAt.Value >= today - && expiresAt.Value <= today.AddDays(30); - - // 2.1 (空行后) 写入列表 - items.Add(new StoreQualificationDto - { - Id = reader.GetInt64(reader.GetOrdinal("Id")), - StoreId = reader.GetInt64(reader.GetOrdinal("StoreId")), - QualificationType = (StoreQualificationType)reader.GetInt32(reader.GetOrdinal("QualificationType")), - FileUrl = reader.GetString(reader.GetOrdinal("FileUrl")), - DocumentNumber = reader.IsDBNull(reader.GetOrdinal("DocumentNumber")) - ? null - : reader.GetString(reader.GetOrdinal("DocumentNumber")), - IssuedAt = reader.IsDBNull(reader.GetOrdinal("IssuedAt")) - ? null - : DateOnly.FromDateTime(reader.GetDateTime(reader.GetOrdinal("IssuedAt"))), - ExpiresAt = expiresAt, - IsExpired = isExpired, - IsExpiringSoon = isExpiringSoon, - DaysUntilExpiry = daysUntilExpiry, - SortOrder = reader.GetInt32(reader.GetOrdinal("SortOrder")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), - UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) - ? null - : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")) - }); - } - - // 3. (空行后) 返回结果 - return items; - } - - private static async Task> QueryAuditRecordsAsync( - IDbConnection connection, - long storeId, - CancellationToken cancellationToken) - { - // 1. 查询审核记录 - await using var command = CreateCommand( - connection, - BuildAuditRecordSql(), - [ - ("storeId", storeId) - ]); - - await using var reader = await command.ExecuteReaderAsync(cancellationToken); - var items = new List(); - if (!reader.HasRows) - { - return items; - } - - // 2. (空行后) 映射审核记录 DTO - while (await reader.ReadAsync(cancellationToken)) - { - var action = (StoreAuditAction)reader.GetInt32(reader.GetOrdinal("Action")); - items.Add(new StoreAuditRecordDto - { - Id = reader.GetInt64(reader.GetOrdinal("Id")), - Action = action, - ActionName = StoreAuditActionNameResolver.Resolve(action), - OperatorId = reader.IsDBNull(reader.GetOrdinal("OperatorId")) - ? null - : reader.GetInt64(reader.GetOrdinal("OperatorId")), - OperatorName = reader.GetString(reader.GetOrdinal("OperatorName")), - PreviousStatus = reader.IsDBNull(reader.GetOrdinal("PreviousStatus")) - ? null - : (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("PreviousStatus")), - NewStatus = (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("NewStatus")), - RejectionReasonId = reader.IsDBNull(reader.GetOrdinal("RejectionReasonId")) - ? null - : reader.GetInt64(reader.GetOrdinal("RejectionReasonId")), - RejectionReasonText = reader.IsDBNull(reader.GetOrdinal("RejectionReason")) - ? null - : reader.GetString(reader.GetOrdinal("RejectionReason")), - Remark = reader.IsDBNull(reader.GetOrdinal("Remarks")) - ? null - : reader.GetString(reader.GetOrdinal("Remarks")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")) - }); - } - - // 3. (空行后) 返回结果 - return items; - } - - private static string BuildStoreSql() - { - return """ - select - s."Id" as "StoreId", - s."Name" as "StoreName", - s."Code" as "StoreCode", - s."Phone", - s."SignboardImageUrl", - s."Province", - s."City", - s."District", - s."Address", - s."Longitude", - s."Latitude", - s."OwnershipType", - s."AuditStatus", - s."SubmittedAt", - s."TenantId", - t."Name" as "TenantName", - t."ContactName" as "TenantContactName", - t."ContactPhone" as "TenantContactPhone", - s."MerchantId", - m."BrandName" as "MerchantName", - m."LegalPerson" as "MerchantLegalName", - m."TaxNumber" as "MerchantCreditCode" - from public.stores s - join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null - join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null - where s."DeletedAt" is null - and s."Id" = @storeId; - """; - } - - private static string BuildQualificationSql() - { - return """ - select - q."Id", - q."StoreId", - q."QualificationType", - q."FileUrl", - q."DocumentNumber", - q."IssuedAt", - q."ExpiresAt", - q."SortOrder", - q."CreatedAt", - q."UpdatedAt" - from public.store_qualifications q - where q."DeletedAt" is null - and q."StoreId" = @storeId - order by q."SortOrder", q."QualificationType"; - """; - } - - private static string BuildAuditRecordSql() - { - return """ - select - r."Id", - r."Action", - r."OperatorId", - r."OperatorName", - r."PreviousStatus", - r."NewStatus", - r."RejectionReasonId", - r."RejectionReason", - r."Remarks", - r."CreatedAt" - from public.store_audit_records r - where r."DeletedAt" is null - and r."StoreId" = @storeId - order by r."CreatedAt" desc; - """; - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditStatisticsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditStatisticsQueryHandler.cs deleted file mode 100644 index f710a79..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditStatisticsQueryHandler.cs +++ /dev/null @@ -1,279 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Application.App.StoreAudits.Queries; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 审核统计查询处理器。 -/// -public sealed class GetStoreAuditStatisticsQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler -{ - /// - public async Task Handle(GetStoreAuditStatisticsQuery request, CancellationToken cancellationToken) - { - // 1. 规范化日期范围 - var today = DateTime.UtcNow.Date; - var dateFrom = request.DateFrom?.Date ?? today.AddDays(-30); - var dateTo = request.DateTo?.Date ?? today; - if (dateFrom > dateTo) - { - (dateFrom, dateTo) = (dateTo, dateFrom); - } - - // 1.1 (空行后) 计算统计边界 - var dateToExclusive = dateTo.AddDays(1); - var overdueDeadline = DateTime.UtcNow.AddDays(-7); - - // 2. (空行后) 查询统计 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 2.1 读取待审核与超时数量 - var pendingCount = await ExecuteScalarIntAsync( - connection, - BuildPendingCountSql(), - [ - ("pendingStatus", (int)StoreAuditStatus.Pending) - ], - token); - var overdueCount = await ExecuteScalarIntAsync( - connection, - BuildOverdueCountSql(), - [ - ("pendingStatus", (int)StoreAuditStatus.Pending), - ("overdueDeadline", overdueDeadline) - ], - token); - - // 2.2 (空行后) 读取通过/驳回数量 - var (approvedCount, rejectedCount) = await QueryApproveRejectCountsAsync( - connection, - dateFrom, - dateToExclusive, - token); - - // 2.3 (空行后) 读取平均处理时长 - var avgProcessingHours = await ExecuteScalarDoubleAsync( - connection, - BuildAvgProcessingSql(), - [ - ("dateFrom", dateFrom), - ("dateTo", dateToExclusive), - ("approveAction", (int)StoreAuditAction.Approve), - ("rejectAction", (int)StoreAuditAction.Reject) - ], - token); - - // 2.4 (空行后) 读取每日趋势 - var dailyTrend = await QueryDailyTrendAsync( - connection, - dateFrom, - dateToExclusive, - token); - - // 2.5 (空行后) 组装结果 - return new StoreAuditStatisticsDto - { - PendingCount = pendingCount, - OverdueCount = overdueCount, - ApprovedCount = approvedCount, - RejectedCount = rejectedCount, - AvgProcessingHours = avgProcessingHours, - DailyTrend = dailyTrend - }; - }, - cancellationToken); - } - - private static async Task<(int ApprovedCount, int RejectedCount)> QueryApproveRejectCountsAsync( - IDbConnection connection, - DateTime dateFrom, - DateTime dateTo, - CancellationToken cancellationToken) - { - // 1. 查询通过/驳回统计 - await using var command = CreateCommand( - connection, - BuildApproveRejectCountSql(), - [ - ("dateFrom", dateFrom), - ("dateTo", dateTo), - ("approveAction", (int)StoreAuditAction.Approve), - ("rejectAction", (int)StoreAuditAction.Reject) - ]); - - await using var reader = await command.ExecuteReaderAsync(cancellationToken); - if (!await reader.ReadAsync(cancellationToken)) - { - return (0, 0); - } - - // 2. (空行后) 返回统计 - var approved = reader.IsDBNull(reader.GetOrdinal("ApprovedCount")) ? 0 : reader.GetInt32(reader.GetOrdinal("ApprovedCount")); - var rejected = reader.IsDBNull(reader.GetOrdinal("RejectedCount")) ? 0 : reader.GetInt32(reader.GetOrdinal("RejectedCount")); - return (approved, rejected); - } - - private static async Task> QueryDailyTrendAsync( - IDbConnection connection, - DateTime dateFrom, - DateTime dateTo, - CancellationToken cancellationToken) - { - // 1. 查询每日趋势 - await using var command = CreateCommand( - connection, - BuildDailyTrendSql(), - [ - ("dateFrom", dateFrom), - ("dateTo", dateTo), - ("submitAction", (int)StoreAuditAction.Submit), - ("resubmitAction", (int)StoreAuditAction.Resubmit), - ("approveAction", (int)StoreAuditAction.Approve), - ("rejectAction", (int)StoreAuditAction.Reject) - ]); - - await using var reader = await command.ExecuteReaderAsync(cancellationToken); - var items = new List(); - if (!reader.HasRows) - { - return items; - } - - // 2. (空行后) 映射趋势项 - var dateOrdinal = reader.GetOrdinal("Date"); - var submittedOrdinal = reader.GetOrdinal("SubmittedCount"); - var approvedOrdinal = reader.GetOrdinal("ApprovedCount"); - var rejectedOrdinal = reader.GetOrdinal("RejectedCount"); - while (await reader.ReadAsync(cancellationToken)) - { - var date = reader.GetDateTime(dateOrdinal); - items.Add(new StoreAuditDailyTrendDto - { - Date = DateOnly.FromDateTime(date), - Submitted = reader.GetInt32(submittedOrdinal), - Approved = reader.GetInt32(approvedOrdinal), - Rejected = reader.GetInt32(rejectedOrdinal) - }); - } - - // 3. (空行后) 返回趋势列表 - return items; - } - - private static string BuildPendingCountSql() - { - return """ - select count(*) - from public.stores s - where s."DeletedAt" is null - and s."AuditStatus" = @pendingStatus; - """; - } - - private static string BuildOverdueCountSql() - { - return """ - select count(*) - from public.stores s - where s."DeletedAt" is null - and s."AuditStatus" = @pendingStatus - and s."SubmittedAt" <= @overdueDeadline; - """; - } - - private static string BuildApproveRejectCountSql() - { - return """ - select - sum(case when r."Action" = @approveAction then 1 else 0 end) as "ApprovedCount", - sum(case when r."Action" = @rejectAction then 1 else 0 end) as "RejectedCount" - from public.store_audit_records r - where r."DeletedAt" is null - and r."CreatedAt" >= @dateFrom - and r."CreatedAt" < @dateTo - and r."Action" in (@approveAction, @rejectAction); - """; - } - - private static string BuildAvgProcessingSql() - { - return """ - select - avg(extract(epoch from (r."CreatedAt" - s."SubmittedAt")) / 3600.0) as "AvgHours" - from public.store_audit_records r - join public.stores s on s."Id" = r."StoreId" and s."DeletedAt" is null - where r."DeletedAt" is null - and r."Action" in (@approveAction, @rejectAction) - and r."CreatedAt" >= @dateFrom - and r."CreatedAt" < @dateTo - and s."SubmittedAt" is not null; - """; - } - - private static string BuildDailyTrendSql() - { - return """ - select - date_trunc('day', r."CreatedAt") as "Date", - sum(case when r."Action" in (@submitAction, @resubmitAction) then 1 else 0 end) as "SubmittedCount", - sum(case when r."Action" = @approveAction then 1 else 0 end) as "ApprovedCount", - sum(case when r."Action" = @rejectAction then 1 else 0 end) as "RejectedCount" - from public.store_audit_records r - where r."DeletedAt" is null - and r."CreatedAt" >= @dateFrom - and r."CreatedAt" < @dateTo - group by date_trunc('day', r."CreatedAt") - order by date_trunc('day', r."CreatedAt"); - """; - } - - private static async Task ExecuteScalarIntAsync( - IDbConnection connection, - string sql, - (string Name, object? Value)[] parameters, - CancellationToken cancellationToken) - { - await using var command = CreateCommand(connection, sql, parameters); - var result = await command.ExecuteScalarAsync(cancellationToken); - return result is null or DBNull ? 0 : Convert.ToInt32(result); - } - - private static async Task ExecuteScalarDoubleAsync( - IDbConnection connection, - string sql, - (string Name, object? Value)[] parameters, - CancellationToken cancellationToken) - { - await using var command = CreateCommand(connection, sql, parameters); - var result = await command.ExecuteScalarAsync(cancellationToken); - return result is null or DBNull ? 0d : Convert.ToDouble(result); - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListPendingStoreAuditsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListPendingStoreAuditsQueryHandler.cs deleted file mode 100644 index 1973112..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListPendingStoreAuditsQueryHandler.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Application.App.StoreAudits.Queries; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 待审核门店列表查询处理器。 -/// -public sealed class ListPendingStoreAuditsQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - public async Task> Handle(ListPendingStoreAuditsQuery request, CancellationToken cancellationToken) - { - // 1. 参数规范化 - var page = request.Page <= 0 ? 1 : request.Page; - var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize; - var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim(); - var offset = (page - 1) * pageSize; - var now = DateTime.UtcNow; - var overdueDeadline = now.AddDays(-7); - - // 2. (空行后) 排序白名单 - var orderBy = request.SortBy?.Trim() switch - { - "StoreName" => "s.\"Name\"", - "MerchantName" => "m.\"BrandName\"", - "TenantName" => "t.\"Name\"", - "SubmittedAt" => "s.\"SubmittedAt\"", - _ => "s.\"SubmittedAt\"" - }; - - // 3. (空行后) 执行查询 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 3.1 统计总数 - var total = await ExecuteScalarIntAsync( - connection, - BuildCountSql(), - [ - ("tenantId", request.TenantId), - ("keyword", keyword), - ("submittedFrom", request.SubmittedFrom), - ("submittedTo", request.SubmittedTo), - ("overdueOnly", request.OverdueOnly), - ("overdueDeadline", overdueDeadline), - ("pendingStatus", (int)StoreAuditStatus.Pending) - ], - token); - - // 3.2 (空行后) 查询列表 - var listSql = BuildListSql(orderBy, request.SortDesc); - await using var listCommand = CreateCommand( - connection, - listSql, - [ - ("tenantId", request.TenantId), - ("keyword", keyword), - ("submittedFrom", request.SubmittedFrom), - ("submittedTo", request.SubmittedTo), - ("overdueOnly", request.OverdueOnly), - ("overdueDeadline", overdueDeadline), - ("pendingStatus", (int)StoreAuditStatus.Pending), - ("offset", offset), - ("limit", pageSize) - ]); - - await using var reader = await listCommand.ExecuteReaderAsync(token); - - // 3.3 (空行后) 读取并映射 - var items = new List(); - if (!reader.HasRows) - { - return new PagedResult(items, page, pageSize, total); - } - - // 3.3.1 (空行后) 初始化字段序号 - var storeIdOrdinal = reader.GetOrdinal("StoreId"); - var storeNameOrdinal = reader.GetOrdinal("StoreName"); - var storeCodeOrdinal = reader.GetOrdinal("StoreCode"); - var tenantIdOrdinal = reader.GetOrdinal("TenantId"); - var tenantNameOrdinal = reader.GetOrdinal("TenantName"); - var merchantIdOrdinal = reader.GetOrdinal("MerchantId"); - var merchantNameOrdinal = reader.GetOrdinal("MerchantName"); - var signboardOrdinal = reader.GetOrdinal("SignboardImageUrl"); - var provinceOrdinal = reader.GetOrdinal("Province"); - var cityOrdinal = reader.GetOrdinal("City"); - var districtOrdinal = reader.GetOrdinal("District"); - var addressOrdinal = reader.GetOrdinal("Address"); - var ownershipOrdinal = reader.GetOrdinal("OwnershipType"); - var submittedAtOrdinal = reader.GetOrdinal("SubmittedAt"); - var qualificationCountOrdinal = reader.GetOrdinal("QualificationCount"); - - while (await reader.ReadAsync(token)) - { - DateTime? submittedAt = reader.IsDBNull(submittedAtOrdinal) - ? null - : reader.GetDateTime(submittedAtOrdinal); - var waitingDays = submittedAt.HasValue - ? (int)Math.Floor((now - submittedAt.Value).TotalDays) - : 0; - if (waitingDays < 0) - { - waitingDays = 0; - } - - // 3.3.2 (空行后) 组装地址信息 - var province = reader.IsDBNull(provinceOrdinal) ? null : reader.GetString(provinceOrdinal); - var city = reader.IsDBNull(cityOrdinal) ? null : reader.GetString(cityOrdinal); - var district = reader.IsDBNull(districtOrdinal) ? null : reader.GetString(districtOrdinal); - var address = reader.IsDBNull(addressOrdinal) ? null : reader.GetString(addressOrdinal); - var fullAddress = string.Concat( - province ?? string.Empty, - city ?? string.Empty, - district ?? string.Empty, - address ?? string.Empty); - - items.Add(new PendingStoreAuditDto - { - StoreId = reader.GetInt64(storeIdOrdinal), - StoreName = reader.GetString(storeNameOrdinal), - StoreCode = reader.GetString(storeCodeOrdinal), - TenantId = reader.GetInt64(tenantIdOrdinal), - TenantName = reader.GetString(tenantNameOrdinal), - MerchantId = reader.GetInt64(merchantIdOrdinal), - MerchantName = reader.GetString(merchantNameOrdinal), - SignboardImageUrl = reader.IsDBNull(signboardOrdinal) ? null : reader.GetString(signboardOrdinal), - FullAddress = fullAddress, - OwnershipType = (StoreOwnershipType)reader.GetInt32(ownershipOrdinal), - SubmittedAt = submittedAt, - WaitingDays = waitingDays, - IsOverdue = submittedAt.HasValue && submittedAt.Value <= overdueDeadline, - QualificationCount = reader.GetInt32(qualificationCountOrdinal) - }); - } - - // 3.4 (空行后) 返回分页结果 - return new PagedResult(items, page, pageSize, total); - }, - cancellationToken); - } - - private static string BuildCountSql() - { - return """ - select count(*) - from public.stores s - join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null - join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null - where s."DeletedAt" is null - and s."AuditStatus" = @pendingStatus - and (@tenantId::bigint is null or s."TenantId" = @tenantId) - and ( - @keyword::text is null - or s."Name" ilike ('%' || @keyword::text || '%') - or s."Code" ilike ('%' || @keyword::text || '%') - or m."BrandName" ilike ('%' || @keyword::text || '%') - ) - and (@submittedFrom::timestamp with time zone is null or s."SubmittedAt" >= @submittedFrom) - and (@submittedTo::timestamp with time zone is null or s."SubmittedAt" <= @submittedTo) - and (@overdueOnly::boolean = false or s."SubmittedAt" <= @overdueDeadline); - """; - } - - private static string BuildListSql(string orderBy, bool sortDesc) - { - var direction = sortDesc ? "desc" : "asc"; - - // 1. (空行后) 构造列表 SQL - return $""" - select - s."Id" as "StoreId", - s."Name" as "StoreName", - s."Code" as "StoreCode", - s."TenantId", - t."Name" as "TenantName", - s."MerchantId", - m."BrandName" as "MerchantName", - s."SignboardImageUrl", - s."Province", - s."City", - s."District", - s."Address", - s."OwnershipType", - s."SubmittedAt", - ( - select count(1) - from public.store_qualifications q - where q."StoreId" = s."Id" and q."DeletedAt" is null - ) as "QualificationCount" - from public.stores s - join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null - join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null - where s."DeletedAt" is null - and s."AuditStatus" = @pendingStatus - and (@tenantId::bigint is null or s."TenantId" = @tenantId) - and ( - @keyword::text is null - or s."Name" ilike ('%' || @keyword::text || '%') - or s."Code" ilike ('%' || @keyword::text || '%') - or m."BrandName" ilike ('%' || @keyword::text || '%') - ) - and (@submittedFrom::timestamp with time zone is null or s."SubmittedAt" >= @submittedFrom) - and (@submittedTo::timestamp with time zone is null or s."SubmittedAt" <= @submittedTo) - and (@overdueOnly::boolean = false or s."SubmittedAt" <= @overdueDeadline) - order by {orderBy} {direction} - offset @offset - limit @limit; - """; - } - - private static async Task ExecuteScalarIntAsync( - IDbConnection connection, - string sql, - (string Name, object? Value)[] parameters, - CancellationToken cancellationToken) - { - await using var command = CreateCommand(connection, sql, parameters); - var result = await command.ExecuteScalarAsync(cancellationToken); - return result is null or DBNull ? 0 : Convert.ToInt32(result); - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListStoreAuditRecordsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListStoreAuditRecordsQueryHandler.cs deleted file mode 100644 index 8731d0b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListStoreAuditRecordsQueryHandler.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Application.App.StoreAudits.Queries; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 门店审核记录查询处理器。 -/// -public sealed class ListStoreAuditRecordsQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - public async Task> Handle(ListStoreAuditRecordsQuery request, CancellationToken cancellationToken) - { - // 1. 参数规范化 - var page = request.Page <= 0 ? 1 : request.Page; - var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize; - var offset = (page - 1) * pageSize; - - // 2. (空行后) 查询审核记录 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 2.1 统计总数 - var total = await ExecuteScalarIntAsync( - connection, - BuildCountSql(), - [ - ("storeId", request.StoreId) - ], - token); - - // 2.2 (空行后) 查询列表 - await using var listCommand = CreateCommand( - connection, - BuildListSql(), - [ - ("storeId", request.StoreId), - ("offset", offset), - ("limit", pageSize) - ]); - - await using var reader = await listCommand.ExecuteReaderAsync(token); - - // 2.3 (空行后) 映射列表 - var items = new List(); - if (!reader.HasRows) - { - return new PagedResult(items, page, pageSize, total); - } - - // 2.3.1 (空行后) 初始化字段序号 - var idOrdinal = reader.GetOrdinal("Id"); - var actionOrdinal = reader.GetOrdinal("Action"); - var operatorIdOrdinal = reader.GetOrdinal("OperatorId"); - var operatorNameOrdinal = reader.GetOrdinal("OperatorName"); - var previousStatusOrdinal = reader.GetOrdinal("PreviousStatus"); - var newStatusOrdinal = reader.GetOrdinal("NewStatus"); - var rejectionReasonIdOrdinal = reader.GetOrdinal("RejectionReasonId"); - var rejectionReasonOrdinal = reader.GetOrdinal("RejectionReason"); - var remarksOrdinal = reader.GetOrdinal("Remarks"); - var createdAtOrdinal = reader.GetOrdinal("CreatedAt"); - - while (await reader.ReadAsync(token)) - { - var action = (StoreAuditAction)reader.GetInt32(actionOrdinal); - items.Add(new StoreAuditRecordDto - { - Id = reader.GetInt64(idOrdinal), - Action = action, - ActionName = StoreAuditActionNameResolver.Resolve(action), - OperatorId = reader.IsDBNull(operatorIdOrdinal) ? null : reader.GetInt64(operatorIdOrdinal), - OperatorName = reader.GetString(operatorNameOrdinal), - PreviousStatus = reader.IsDBNull(previousStatusOrdinal) - ? null - : (StoreAuditStatus)reader.GetInt32(previousStatusOrdinal), - NewStatus = (StoreAuditStatus)reader.GetInt32(newStatusOrdinal), - RejectionReasonId = reader.IsDBNull(rejectionReasonIdOrdinal) - ? null - : reader.GetInt64(rejectionReasonIdOrdinal), - RejectionReasonText = reader.IsDBNull(rejectionReasonOrdinal) - ? null - : reader.GetString(rejectionReasonOrdinal), - Remark = reader.IsDBNull(remarksOrdinal) ? null : reader.GetString(remarksOrdinal), - CreatedAt = reader.GetDateTime(createdAtOrdinal) - }); - } - - // 2.4 (空行后) 返回分页结果 - return new PagedResult(items, page, pageSize, total); - }, - cancellationToken); - } - - private static string BuildCountSql() - { - return """ - select count(*) - from public.store_audit_records r - where r."DeletedAt" is null - and r."StoreId" = @storeId; - """; - } - - private static string BuildListSql() - { - return """ - select - r."Id", - r."Action", - r."OperatorId", - r."OperatorName", - r."PreviousStatus", - r."NewStatus", - r."RejectionReasonId", - r."RejectionReason", - r."Remarks", - r."CreatedAt" - from public.store_audit_records r - where r."DeletedAt" is null - and r."StoreId" = @storeId - order by r."CreatedAt" desc - offset @offset - limit @limit; - """; - } - - private static async Task ExecuteScalarIntAsync( - IDbConnection connection, - string sql, - (string Name, object? Value)[] parameters, - CancellationToken cancellationToken) - { - await using var command = CreateCommand(connection, sql, parameters); - var result = await command.ExecuteScalarAsync(cancellationToken); - return result is null or DBNull ? 0 : Convert.ToInt32(result); - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/RejectStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/RejectStoreCommandHandler.cs deleted file mode 100644 index fdc6ec5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/RejectStoreCommandHandler.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.StoreAudits.Commands; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Domain.Stores.Entities; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 审核驳回处理器。 -/// -public sealed class RejectStoreCommandHandler( - IStoreRepository storeRepository, - IDapperExecutor dapperExecutor, - ICurrentUserAccessor currentUserAccessor, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(RejectStoreCommand request, CancellationToken cancellationToken) - { - // 1. 获取门店快照 - var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken); - if (!snapshot.HasValue) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 1.1 (空行后) 校验审核状态 - if (snapshot.Value.AuditStatus != StoreAuditStatus.Pending) - { - throw new BusinessException(ErrorCodes.Conflict, "门店不处于待审核状态"); - } - - // 2. (空行后) 获取门店实体 - var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken); - if (store is null) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 3. (空行后) 更新状态并记录审核 - var previousStatus = store.AuditStatus; - store.AuditStatus = StoreAuditStatus.Rejected; - store.RejectionReason = request.RejectionReasonText; - store.BusinessStatus = StoreBusinessStatus.Resting; - - await storeRepository.UpdateStoreAsync(store, cancellationToken); - await storeRepository.AddAuditRecordAsync(new StoreAuditRecord - { - StoreId = store.Id, - Action = StoreAuditAction.Reject, - PreviousStatus = previousStatus, - NewStatus = store.AuditStatus, - OperatorId = ResolveOperatorId(), - OperatorName = ResolveOperatorName(), - RejectionReasonId = request.RejectionReasonId, - RejectionReason = request.RejectionReasonText, - Remarks = request.Remark - }, cancellationToken); - await storeRepository.SaveChangesAsync(cancellationToken); - logger.LogInformation("门店 {StoreId} 审核驳回", store.Id); - - // 4. (空行后) 返回结果 - return new StoreAuditActionResultDto - { - StoreId = store.Id, - AuditStatus = store.AuditStatus, - BusinessStatus = store.BusinessStatus, - RejectionReason = store.RejectionReason, - Message = "已驳回,租户可修改后重新提交" - }; - } - - private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync( - long storeId, - CancellationToken cancellationToken) - { - // 1. 查询门店基础字段 - return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - await using var command = CreateCommand( - connection, - BuildStoreSnapshotSql(), - [ - ("storeId", storeId) - ]); - - // 1.1 (空行后) 执行查询 - await using var reader = await command.ExecuteReaderAsync(token); - if (!await reader.ReadAsync(token)) - { - return null; - } - - // 1.2 (空行后) 返回快照 - return ( - reader.GetInt64(reader.GetOrdinal("TenantId")), - (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")), - (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus"))); - }, - cancellationToken); - } - - private long? ResolveOperatorId() - { - var id = currentUserAccessor.UserId; - return id == 0 ? null : id; - } - - private string ResolveOperatorName() - { - var id = currentUserAccessor.UserId; - return id == 0 ? "system" : $"user:{id}"; - } - - private static string BuildStoreSnapshotSql() - { - return """ - select - s."TenantId", - s."AuditStatus", - s."BusinessStatus" - from public.stores s - where s."DeletedAt" is null - and s."Id" = @storeId; - """; - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ReopenStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ReopenStoreCommandHandler.cs deleted file mode 100644 index a02d535..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ReopenStoreCommandHandler.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Data; -using System.Data.Common; -using MediatR; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.StoreAudits.Commands; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Domain.Stores.Entities; -using TakeoutSaaS.Domain.Stores.Enums; -using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.StoreAudits.Handlers; - -/// -/// 解除强制关闭处理器。 -/// -public sealed class ReopenStoreCommandHandler( - IStoreRepository storeRepository, - IDapperExecutor dapperExecutor, - ICurrentUserAccessor currentUserAccessor, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(ReopenStoreCommand request, CancellationToken cancellationToken) - { - // 1. 获取门店快照 - var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken); - if (!snapshot.HasValue) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 1.1 (空行后) 校验审核与经营状态 - if (snapshot.Value.AuditStatus != StoreAuditStatus.Activated) - { - throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法解除关闭"); - } - - if (snapshot.Value.BusinessStatus != StoreBusinessStatus.ForceClosed) - { - throw new BusinessException(ErrorCodes.Conflict, "门店未处于强制关闭状态"); - } - - // 2. (空行后) 获取门店实体 - var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken); - if (store is null) - { - throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); - } - - // 3. (空行后) 更新状态并记录风控 - store.BusinessStatus = StoreBusinessStatus.Resting; - store.ClosureReason = null; - store.ClosureReasonText = null; - store.ForceCloseReason = null; - store.ForceClosedAt = null; - - await storeRepository.UpdateStoreAsync(store, cancellationToken); - await storeRepository.AddAuditRecordAsync(new StoreAuditRecord - { - StoreId = store.Id, - Action = StoreAuditAction.Reopen, - PreviousStatus = store.AuditStatus, - NewStatus = store.AuditStatus, - OperatorId = ResolveOperatorId(), - OperatorName = ResolveOperatorName(), - Remarks = request.Remark - }, cancellationToken); - await storeRepository.SaveChangesAsync(cancellationToken); - logger.LogInformation("门店 {StoreId} 解除强制关闭", store.Id); - - // 4. (空行后) 返回结果 - return new StoreAuditActionResultDto - { - StoreId = store.Id, - AuditStatus = store.AuditStatus, - BusinessStatus = store.BusinessStatus, - Message = "强制关闭已解除,门店恢复为休息中状态" - }; - } - - private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync( - long storeId, - CancellationToken cancellationToken) - { - // 1. 查询门店基础字段 - return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - await using var command = CreateCommand( - connection, - BuildStoreSnapshotSql(), - [ - ("storeId", storeId) - ]); - - // 1.1 (空行后) 执行查询 - await using var reader = await command.ExecuteReaderAsync(token); - if (!await reader.ReadAsync(token)) - { - return null; - } - - // 1.2 (空行后) 返回快照 - return ( - reader.GetInt64(reader.GetOrdinal("TenantId")), - (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")), - (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus"))); - }, - cancellationToken); - } - - private long? ResolveOperatorId() - { - var id = currentUserAccessor.UserId; - return id == 0 ? null : id; - } - - private string ResolveOperatorName() - { - var id = currentUserAccessor.UserId; - return id == 0 ? "system" : $"user:{id}"; - } - - private static string BuildStoreSnapshotSql() - { - return """ - select - s."TenantId", - s."AuditStatus", - s."BusinessStatus" - from public.stores s - where s."DeletedAt" is null - and s."Id" = @storeId; - """; - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - // 1. (空行后) 绑定参数 - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditDetailQuery.cs deleted file mode 100644 index 17e528c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditDetailQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; - -namespace TakeoutSaaS.Application.App.StoreAudits.Queries; - -/// -/// 获取门店审核详情查询。 -/// -public sealed record GetStoreAuditDetailQuery : IRequest -{ - /// - /// 门店 ID。 - /// - public long StoreId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditStatisticsQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditStatisticsQuery.cs deleted file mode 100644 index c756851..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditStatisticsQuery.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; - -namespace TakeoutSaaS.Application.App.StoreAudits.Queries; - -/// -/// 获取审核统计查询。 -/// -public sealed record GetStoreAuditStatisticsQuery : IRequest -{ - /// - /// 起始日期。 - /// - public DateTime? DateFrom { get; init; } - - /// - /// 截止日期。 - /// - public DateTime? DateTo { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListPendingStoreAuditsQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListPendingStoreAuditsQuery.cs deleted file mode 100644 index a7f2748..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListPendingStoreAuditsQuery.cs +++ /dev/null @@ -1,56 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.StoreAudits.Queries; - -/// -/// 查询待审核门店列表。 -/// -public sealed record ListPendingStoreAuditsQuery : IRequest> -{ - /// - /// 租户 ID。 - /// - public long? TenantId { get; init; } - - /// - /// 关键词。 - /// - public string? Keyword { get; init; } - - /// - /// 提交起始时间。 - /// - public DateTime? SubmittedFrom { get; init; } - - /// - /// 提交截止时间。 - /// - public DateTime? SubmittedTo { get; init; } - - /// - /// 是否只显示超时。 - /// - public bool OverdueOnly { get; init; } - - /// - /// 页码。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页数量。 - /// - public int PageSize { get; init; } = 20; - - /// - /// 排序字段。 - /// - public string? SortBy { get; init; } - - /// - /// 是否降序。 - /// - public bool SortDesc { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListStoreAuditRecordsQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListStoreAuditRecordsQuery.cs deleted file mode 100644 index ac05c3a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListStoreAuditRecordsQuery.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.StoreAudits.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.StoreAudits.Queries; - -/// -/// 查询门店审核记录。 -/// -public sealed record ListStoreAuditRecordsQuery : IRequest> -{ - /// - /// 门店 ID。 - /// - public long StoreId { get; init; } - - /// - /// 页码。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页数量。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/StoreAuditActionNameResolver.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/StoreAuditActionNameResolver.cs deleted file mode 100644 index c4f35f4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/StoreAuditActionNameResolver.cs +++ /dev/null @@ -1,26 +0,0 @@ -using TakeoutSaaS.Domain.Stores.Enums; - -namespace TakeoutSaaS.Application.App.StoreAudits; - -/// -/// 门店审核动作名称解析器。 -/// -public static class StoreAuditActionNameResolver -{ - /// - /// 获取动作名称。 - /// - /// 审核动作。 - /// 动作名称。 - public static string Resolve(StoreAuditAction action) => action switch - { - StoreAuditAction.Submit => "提交审核", - StoreAuditAction.Resubmit => "重新提交", - StoreAuditAction.Approve => "审核通过", - StoreAuditAction.Reject => "审核驳回", - StoreAuditAction.ForceClose => "强制关闭", - StoreAuditAction.Reopen => "解除关闭", - StoreAuditAction.AutoActivate => "自动激活", - _ => "未知操作" - }; -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ApproveStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ApproveStoreCommandValidator.cs deleted file mode 100644 index fddba8b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ApproveStoreCommandValidator.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.StoreAudits.Commands; - -namespace TakeoutSaaS.Application.App.StoreAudits.Validators; - -/// -/// 审核通过命令验证器。 -/// -public sealed class ApproveStoreCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public ApproveStoreCommandValidator() - { - RuleFor(x => x.StoreId).GreaterThan(0); - RuleFor(x => x.Remark).MaximumLength(500); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ForceCloseStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ForceCloseStoreCommandValidator.cs deleted file mode 100644 index 31e3db9..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ForceCloseStoreCommandValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.StoreAudits.Commands; - -namespace TakeoutSaaS.Application.App.StoreAudits.Validators; - -/// -/// 强制关闭命令验证器。 -/// -public sealed class ForceCloseStoreCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public ForceCloseStoreCommandValidator() - { - RuleFor(x => x.StoreId).GreaterThan(0); - RuleFor(x => x.Reason).NotEmpty().MaximumLength(500); - RuleFor(x => x.Remark).MaximumLength(500); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/RejectStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/RejectStoreCommandValidator.cs deleted file mode 100644 index 8cf278e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/RejectStoreCommandValidator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.StoreAudits.Commands; - -namespace TakeoutSaaS.Application.App.StoreAudits.Validators; - -/// -/// 审核驳回命令验证器。 -/// -public sealed class RejectStoreCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public RejectStoreCommandValidator() - { - RuleFor(x => x.StoreId).GreaterThan(0); - RuleFor(x => x.RejectionReasonId).GreaterThan(0); - RuleFor(x => x.RejectionReasonText).MaximumLength(500); - RuleFor(x => x.Remark).MaximumLength(500); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ReopenStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ReopenStoreCommandValidator.cs deleted file mode 100644 index e44fc46..0000000 --- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ReopenStoreCommandValidator.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.StoreAudits.Commands; - -namespace TakeoutSaaS.Application.App.StoreAudits.Validators; - -/// -/// 解除强制关闭命令验证器。 -/// -public sealed class ReopenStoreCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public ReopenStoreCommandValidator() - { - RuleFor(x => x.StoreId).GreaterThan(0); - RuleFor(x => x.Remark).MaximumLength(500); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs deleted file mode 100644 index 56192f5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 租户初次绑定订阅命令(默认 0 个月)。 -/// -public sealed record BindInitialTenantSubscriptionCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 套餐 ID。 - /// - [Required] - public long TenantPackageId { get; init; } - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ChangeTenantSubscriptionPlanCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ChangeTenantSubscriptionPlanCommand.cs deleted file mode 100644 index 5c052be..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ChangeTenantSubscriptionPlanCommand.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 套餐升降配命令。 -/// -public sealed record ChangeTenantSubscriptionPlanCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 现有订阅 ID。 - /// - [Required] - public long TenantSubscriptionId { get; init; } - - /// - /// 目标套餐 ID。 - /// - [Required] - public long TargetPackageId { get; init; } - - /// - /// 是否立即生效,否则在下一结算周期生效。 - /// - public bool Immediate { get; init; } - - /// - /// 调整备注。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs deleted file mode 100644 index 7d95631..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 领取租户入驻审核命令。 -/// -public sealed record ClaimTenantReviewCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs deleted file mode 100644 index 0005a64..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 生成租户主管理员重置链接令牌命令(平台超级管理员使用)。 -/// -public sealed record CreateTenantAdminResetLinkTokenCommand : IRequest -{ - /// - /// 目标租户 ID。 - /// - public required long TenantId { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantBillingCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantBillingCommand.cs deleted file mode 100644 index 1bc64a7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantBillingCommand.cs +++ /dev/null @@ -1,56 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 创建租户账单命令。 -/// -public sealed record CreateTenantBillingCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 账单编号。 - /// - public string StatementNo { get; init; } = string.Empty; - - /// - /// 计费周期开始时间(UTC)。 - /// - public DateTime PeriodStart { get; init; } - - /// - /// 计费周期结束时间(UTC)。 - /// - public DateTime PeriodEnd { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 已付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 账单状态。 - /// - public TenantBillingStatus Status { get; init; } = TenantBillingStatus.Pending; - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 账单明细 JSON。 - /// - public string? LineItemsJson { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantManuallyCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantManuallyCommand.cs deleted file mode 100644 index f7af18d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantManuallyCommand.cs +++ /dev/null @@ -1,264 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号)。 -/// -public sealed record CreateTenantManuallyCommand : IRequest -{ - /// - /// 租户短编码,作为跨系统引用的唯一标识。 - /// - [Required] - [StringLength(64)] - public string Code { get; init; } = string.Empty; - - /// - /// 租户全称或品牌名称。 - /// - [Required] - [StringLength(128)] - public string Name { get; init; } = string.Empty; - - /// - /// 对外展示的简称。 - /// - public string? ShortName { get; init; } - - /// - /// 法人或公司主体名称。 - /// - public string? LegalEntityName { get; init; } - - /// - /// 所属行业,如餐饮、零售等。 - /// - public string? Industry { get; init; } - - /// - /// LOGO 图片地址。 - /// - public string? LogoUrl { get; init; } - - /// - /// 品牌海报或封面图。 - /// - public string? CoverImageUrl { get; init; } - - /// - /// 官网或主要宣传链接。 - /// - public string? Website { get; init; } - - /// - /// 所在国家/地区。 - /// - public string? Country { get; init; } - - /// - /// 所在省份或州。 - /// - public string? Province { get; init; } - - /// - /// 所在城市。 - /// - public string? City { get; init; } - - /// - /// 详细地址信息。 - /// - public string? Address { get; init; } - - /// - /// 主联系人姓名。 - /// - public string? ContactName { get; init; } - - /// - /// 主联系人电话(唯一)。 - /// - public string? ContactPhone { get; init; } - - /// - /// 主联系人邮箱。 - /// - public string? ContactEmail { get; init; } - - /// - /// 业务标签集合(逗号分隔)。 - /// - public string? Tags { get; init; } - - /// - /// 备注信息,用于运营记录特殊说明。 - /// - public string? Remarks { get; init; } - - /// - /// 最近一次暂停服务时间。 - /// - public DateTime? SuspendedAt { get; init; } - - /// - /// 暂停或终止的原因说明。 - /// - public string? SuspensionReason { get; init; } - - /// - /// 租户当前状态,默认 Active(直接入驻)。 - /// - public TenantStatus TenantStatus { get; init; } = TenantStatus.Active; - - /// - /// 购买套餐 ID。 - /// - [Required] - public long TenantPackageId { get; init; } - - /// - /// 订阅时长(月)。 - /// - [Range(1, int.MaxValue)] - public int DurationMonths { get; init; } = 12; - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } = false; - - /// - /// 订阅生效时间(UTC),为空则立即生效。 - /// - public DateTime? SubscriptionEffectiveFrom { get; init; } - - /// - /// 下次计费时间(UTC),为空则默认等于到期时间。 - /// - public DateTime? NextBillingDate { get; init; } - - /// - /// 订阅状态,默认 Active。 - /// - public SubscriptionStatus SubscriptionStatus { get; init; } = SubscriptionStatus.Active; - - /// - /// 预定下次切换的套餐 ID。 - /// - public long? ScheduledPackageId { get; init; } - - /// - /// 订阅备注。 - /// - public string? SubscriptionNotes { get; init; } - - /// - /// 实名状态,默认 Approved(直接通过)。 - /// - public TenantVerificationStatus VerificationStatus { get; init; } = TenantVerificationStatus.Approved; - - /// - /// 营业执照编号。 - /// - public string? BusinessLicenseNumber { get; init; } - - /// - /// 营业执照扫描件地址。 - /// - public string? BusinessLicenseUrl { get; init; } - - /// - /// 法人姓名。 - /// - public string? LegalPersonName { get; init; } - - /// - /// 法人身份证号。 - /// - public string? LegalPersonIdNumber { get; init; } - - /// - /// 法人身份证人像面图片地址。 - /// - public string? LegalPersonIdFrontUrl { get; init; } - - /// - /// 法人身份证国徽面图片地址。 - /// - public string? LegalPersonIdBackUrl { get; init; } - - /// - /// 对公账户户名。 - /// - public string? BankAccountName { get; init; } - - /// - /// 对公银行账号。 - /// - public string? BankAccountNumber { get; init; } - - /// - /// 开户行名称。 - /// - public string? BankName { get; init; } - - /// - /// 其他补充资料 JSON。 - /// - public string? AdditionalDataJson { get; init; } - - /// - /// 提交时间(UTC),为空则默认当前时间。 - /// - public DateTime? SubmittedAt { get; init; } - - /// - /// 审核时间(UTC),为空则默认当前时间。 - /// - public DateTime? ReviewedAt { get; init; } - - /// - /// 审核人姓名(展示用),为空则默认当前用户。 - /// - public string? ReviewedByName { get; init; } - - /// - /// 审核备注。 - /// - public string? ReviewRemarks { get; init; } - - /// - /// 租户管理员账号。 - /// - [Required] - [StringLength(128)] - public string AdminAccount { get; init; } = string.Empty; - - /// - /// 租户管理员显示名。 - /// - [Required] - [StringLength(128)] - public string AdminDisplayName { get; init; } = string.Empty; - - /// - /// 管理员初始密码(明文,仅用于创建时生成哈希,不会被持久化回传)。 - /// - [Required] - [StringLength(128, MinimumLength = 6)] - public string AdminPassword { get; init; } = string.Empty; - - /// - /// 管理员头像。 - /// - public string? AdminAvatar { get; init; } - - /// - /// 关联商户 ID(若有)。 - /// - public long? AdminMerchantId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantPackageCommand.cs deleted file mode 100644 index 6f709ba..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantPackageCommand.cs +++ /dev/null @@ -1,101 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 创建租户套餐命令。 -/// -public sealed record CreateTenantPackageCommand : IRequest -{ - /// - /// 套餐名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 套餐描述。 - /// - public string? Description { get; init; } - - /// - /// 套餐类型。 - /// - public TenantPackageType PackageType { get; init; } = TenantPackageType.Standard; - - /// - /// 月付价格。 - /// - public decimal? MonthlyPrice { get; init; } - - /// - /// 年付价格。 - /// - public decimal? YearlyPrice { get; init; } - - /// - /// 最大门店数。 - /// - public int? MaxStoreCount { get; init; } - - /// - /// 最大账号数。 - /// - public int? MaxAccountCount { get; init; } - - /// - /// 存储上限(GB)。 - /// - public int? MaxStorageGb { get; init; } - - /// - /// 短信额度。 - /// - public int? MaxSmsCredits { get; init; } - - /// - /// 配送单上限。 - /// - public int? MaxDeliveryOrders { get; init; } - - /// - /// 权益明细 JSON。 - /// - public string? FeaturePoliciesJson { get; init; } - - /// - /// 是否仍启用(平台控制)。 - /// - public bool IsActive { get; init; } = true; - - /// - /// 是否对外可见(展示页/套餐列表可见性)。 - /// - public bool IsPublicVisible { get; init; } = true; - - /// - /// 是否允许新租户购买/选择(仅影响新购)。 - /// - public bool IsAllowNewTenantPurchase { get; init; } = true; - - /// - /// 发布状态(草稿/已发布)。 - /// - public TenantPackagePublishStatus? PublishStatus { get; init; } - - /// - /// 是否推荐展示(运营推荐标识)。 - /// - public bool IsRecommended { get; init; } - - /// - /// 套餐标签(用于展示与对比页)。 - /// - public string[] Tags { get; init; } = []; - - /// - /// 展示排序,数值越小越靠前。 - /// - public int SortOrder { get; init; } = 0; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantSubscriptionCommand.cs deleted file mode 100644 index b31a92d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantSubscriptionCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 新建或续费订阅。 -/// -public sealed record CreateTenantSubscriptionCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 套餐 ID。 - /// - [Required] - public long TenantPackageId { get; init; } - - /// - /// 订阅时长(月)。 - /// - public int DurationMonths { get; init; } - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantPackageCommand.cs deleted file mode 100644 index 9f46a76..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantPackageCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 删除租户套餐命令。 -/// -public sealed record DeleteTenantPackageCommand : IRequest -{ - /// - /// 套餐 ID。 - /// - public long TenantPackageId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ExtendTenantSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ExtendTenantSubscriptionCommand.cs deleted file mode 100644 index cd5c378..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ExtendTenantSubscriptionCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 延期/赠送租户订阅时长(按当前订阅套餐续费)。 -/// -public sealed record ExtendTenantSubscriptionCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 赠送/延期时长(月)。 - /// - [Range(1, 120)] - public int DurationMonths { get; init; } - - /// - /// 备注信息。 - /// - [MaxLength(256)] - public string? Notes { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs deleted file mode 100644 index c782b98..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 强制接管租户入驻审核命令(仅超级管理员可用)。 -/// -public sealed record ForceClaimTenantReviewCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/FreezeTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/FreezeTenantCommand.cs deleted file mode 100644 index c8db6f6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/FreezeTenantCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 冻结租户(将租户状态置为暂停)。 -/// -public sealed record FreezeTenantCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 冻结原因。 - /// - [Required] - [MaxLength(256)] - public string Reason { get; init; } = string.Empty; -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs deleted file mode 100644 index 1ea701a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Contracts; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 伪装登录租户命令(平台超级管理员使用)。 -/// -public sealed record ImpersonateTenantCommand : IRequest -{ - /// - /// 目标租户 ID。 - /// - public required long TenantId { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantBillingPaidCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantBillingPaidCommand.cs deleted file mode 100644 index e2c9f18..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantBillingPaidCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 标记租户账单已支付命令。 -/// -public sealed record MarkTenantBillingPaidCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 账单 ID。 - /// - public long BillingId { get; init; } - - /// - /// 本次支付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 支付时间(UTC)。 - /// - public DateTime PaidAt { get; init; } = DateTime.UtcNow; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RegisterTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RegisterTenantCommand.cs deleted file mode 100644 index d26a96b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RegisterTenantCommand.cs +++ /dev/null @@ -1,71 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 注册租户命令。 -/// -public sealed record RegisterTenantCommand : IRequest -{ - /// - /// 唯一租户编码。 - /// - [Required] - [StringLength(64)] - public string Code { get; init; } = string.Empty; - - /// - /// 租户名称。 - /// - [Required] - [StringLength(128)] - public string Name { get; init; } = string.Empty; - - /// - /// 租户简称。 - /// - public string? ShortName { get; init; } - - /// - /// 行业类型。 - /// - public string? Industry { get; init; } - - /// - /// 联系人姓名。 - /// - public string? ContactName { get; init; } - - /// - /// 联系人电话。 - /// - public string? ContactPhone { get; init; } - - /// - /// 联系人邮箱。 - /// - public string? ContactEmail { get; init; } - - /// - /// 购买套餐 ID。 - /// - [Required] - public long TenantPackageId { get; init; } - - /// - /// 订阅时长(月),默认 12 个月。 - /// - public int DurationMonths { get; init; } = 12; - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } = true; - - /// - /// 生效时间(UTC),为空则立即生效。 - /// - public DateTime? EffectiveFrom { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs deleted file mode 100644 index e6cb24d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 释放租户入驻审核领取命令。 -/// -public sealed record ReleaseTenantReviewClaimCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReviewTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReviewTenantCommand.cs deleted file mode 100644 index 7e7a0ae..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReviewTenantCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Common.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 审核租户命令。 -/// -public sealed record ReviewTenantCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 是否通过审核。 - /// - public bool Approve { get; init; } - - /// - /// 审核备注或拒绝原因。 - /// - public string? Reason { get; init; } - - /// - /// 审核通过后续费时长(月)。 - /// - public int? RenewMonths { get; init; } - - /// - /// 经营模式(审核通过时必填)。 - /// - public OperatingMode? OperatingMode { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs deleted file mode 100644 index 6d99553..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs +++ /dev/null @@ -1,24 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 解冻租户(恢复租户状态)。 -/// -public sealed record UnfreezeTenantCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 解冻备注(可选)。 - /// - [MaxLength(256)] - public string? Reason { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs deleted file mode 100644 index 285a032..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs +++ /dev/null @@ -1,106 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 更新租户套餐命令。 -/// -public sealed record UpdateTenantPackageCommand : IRequest -{ - /// - /// 套餐 ID。 - /// - public long TenantPackageId { get; init; } - - /// - /// 套餐名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 套餐描述。 - /// - public string? Description { get; init; } - - /// - /// 套餐类型。 - /// - public TenantPackageType PackageType { get; init; } = TenantPackageType.Standard; - - /// - /// 月付价格。 - /// - public decimal? MonthlyPrice { get; init; } - - /// - /// 年付价格。 - /// - public decimal? YearlyPrice { get; init; } - - /// - /// 最大门店数。 - /// - public int? MaxStoreCount { get; init; } - - /// - /// 最大账号数。 - /// - public int? MaxAccountCount { get; init; } - - /// - /// 存储上限(GB)。 - /// - public int? MaxStorageGb { get; init; } - - /// - /// 短信额度。 - /// - public int? MaxSmsCredits { get; init; } - - /// - /// 配送单上限。 - /// - public int? MaxDeliveryOrders { get; init; } - - /// - /// 权益明细 JSON。 - /// - public string? FeaturePoliciesJson { get; init; } - - /// - /// 是否仍启用(平台控制)。 - /// - public bool IsActive { get; init; } = true; - - /// - /// 是否对外可见(展示页/套餐列表可见性)。 - /// - public bool IsPublicVisible { get; init; } = true; - - /// - /// 是否允许新租户购买/选择(仅影响新购)。 - /// - public bool IsAllowNewTenantPurchase { get; init; } = true; - - /// - /// 发布状态(草稿/已发布)。 - /// - public TenantPackagePublishStatus? PublishStatus { get; init; } - - /// - /// 是否推荐展示(运营推荐标识)。 - /// - public bool IsRecommended { get; init; } - - /// - /// 套餐标签(用于展示与对比页)。 - /// - public string[] Tags { get; init; } = []; - - /// - /// 展示排序,数值越小越靠前。 - /// - public int SortOrder { get; init; } = 0; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs deleted file mode 100644 index ff3f9dc..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 租户审核日志 DTO。 -/// -public sealed class TenantAuditLogDto -{ - /// - /// 日志 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 动作。 - /// - public TenantAuditAction Action { get; init; } - - /// - /// 标题。 - /// - public string Title { get; init; } = string.Empty; - - /// - /// 描述。 - /// - public string? Description { get; init; } - - /// - /// 操作人。 - /// - public string? OperatorName { get; init; } - - /// - /// 原状态。 - /// - public TenantStatus? PreviousStatus { get; init; } - - /// - /// 新状态。 - /// - public TenantStatus? CurrentStatus { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDetailDto.cs deleted file mode 100644 index 4b642ff..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDetailDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 租户详情 DTO。 -/// -public sealed class TenantDetailDto -{ - /// - /// 基础信息。 - /// - public TenantDto Tenant { get; init; } = new(); - - /// - /// 实名信息。 - /// - public TenantVerificationDto? Verification { get; init; } - - /// - /// 当前订阅。 - /// - public TenantSubscriptionDto? Subscription { get; init; } - - /// - /// 当前套餐详情。 - /// - public TenantPackageDto? Package { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDto.cs deleted file mode 100644 index a565e29..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDto.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Common.Enums; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 租户基础信息 DTO。 -/// -public sealed class TenantDto -{ - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户编码。 - /// - public string Code { get; init; } = string.Empty; - - /// - /// 名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 简称。 - /// - public string? ShortName { get; init; } - - /// - /// 联系人。 - /// - public string? ContactName { get; init; } - - /// - /// 联系电话。 - /// - public string? ContactPhone { get; init; } - - /// - /// 邮箱。 - /// - public string? ContactEmail { get; init; } - - /// - /// 当前状态。 - /// - public TenantStatus Status { get; init; } - - /// - /// 实名状态。 - /// - public TenantVerificationStatus VerificationStatus { get; init; } - - /// - /// 经营模式。 - /// - public OperatingMode? OperatingMode { get; init; } - - /// - /// 当前套餐 ID。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? CurrentPackageId { get; init; } - - /// - /// 当前订阅有效期开始。 - /// - public DateTime? EffectiveFrom { get; init; } - - /// - /// 当前订阅有效期结束。 - /// - public DateTime? EffectiveTo { get; init; } - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageTenantDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageTenantDto.cs deleted file mode 100644 index 9c49c80..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageTenantDto.cs +++ /dev/null @@ -1,50 +0,0 @@ -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 套餐使用租户 DTO(用于平台查看套餐关联租户列表)。 -/// -public sealed class TenantPackageTenantDto -{ - /// - /// 租户 ID。 - /// - public long TenantId { get; init; } - - /// - /// 租户编码。 - /// - public string Code { get; init; } = string.Empty; - - /// - /// 租户名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 租户状态。 - /// - public TenantStatus Status { get; init; } - - /// - /// 联系人。 - /// - public string? ContactName { get; init; } - - /// - /// 联系电话。 - /// - public string? ContactPhone { get; init; } - - /// - /// 当前订阅生效时间(UTC)。 - /// - public DateTime SubscriptionEffectiveFrom { get; init; } - - /// - /// 当前订阅到期时间(UTC)。 - /// - public DateTime SubscriptionEffectiveTo { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs deleted file mode 100644 index cc3f951..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 套餐使用统计 DTO(订阅关联数量、使用租户数量)。 -/// -public sealed class TenantPackageUsageDto -{ - /// - /// 套餐 ID。 - /// - public long TenantPackageId { get; init; } - - /// - /// 当前有效订阅数量(以当前时间为准)。 - /// - public int ActiveSubscriptionCount { get; init; } - - /// - /// 当前使用租户数量(以当前时间为准,按租户去重)。 - /// - public int ActiveTenantCount { get; init; } - - /// - /// 历史总订阅记录数量(不含软删)。 - /// - public int TotalSubscriptionCount { get; init; } - - /// - /// MRR(Monthly Recurring Revenue)粗看:按“当前有效订阅数 × 套餐月付等效价”估算。 - /// - public decimal Mrr { get; init; } - - /// - /// ARR(Annual Recurring Revenue)粗看:按“当前有效订阅数 × 套餐年付等效价”估算。 - /// - public decimal Arr { get; init; } - - /// - /// 未来 7 天内到期的使用租户数量(按租户去重,以当前时间为准,口径:有效订阅)。 - /// - public int ExpiringTenantCount7Days { get; init; } - - /// - /// 未来 15 天内到期的使用租户数量(按租户去重,以当前时间为准,口径:有效订阅)。 - /// - public int ExpiringTenantCount15Days { get; init; } - - /// - /// 未来 30 天内到期的使用租户数量(按租户去重,以当前时间为准,口径:有效订阅)。 - /// - public int ExpiringTenantCount30Days { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantReviewClaimDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantReviewClaimDto.cs deleted file mode 100644 index 565a543..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantReviewClaimDto.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 租户审核领取信息 DTO。 -/// -public sealed class TenantReviewClaimDto -{ - /// - /// 领取记录 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 领取人用户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long ClaimedBy { get; init; } - - /// - /// 领取人名称。 - /// - public string ClaimedByName { get; init; } = string.Empty; - - /// - /// 领取时间。 - /// - public DateTime ClaimedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantSubscriptionDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantSubscriptionDto.cs deleted file mode 100644 index 3699896..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantSubscriptionDto.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 租户订阅 DTO。 -/// -public sealed class TenantSubscriptionDto -{ - /// - /// 订阅 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 套餐 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantPackageId { get; init; } - - /// - /// 状态。 - /// - public SubscriptionStatus Status { get; init; } - - /// - /// 生效时间。 - /// - public DateTime EffectiveFrom { get; init; } - - /// - /// 到期时间。 - /// - public DateTime EffectiveTo { get; init; } - - /// - /// 下次扣费时间。 - /// - public DateTime? NextBillingDate { get; init; } - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs deleted file mode 100644 index 4da9d2c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs +++ /dev/null @@ -1,112 +0,0 @@ -using MediatR; -using System.Collections.Concurrent; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 租户初次绑定订阅处理器。 -/// -public sealed class BindInitialTenantSubscriptionCommandHandler( - ITenantRepository tenantRepository, - IIdGenerator idGenerator, - ITenantProvider tenantProvider) - : IRequestHandler -{ - private static readonly ConcurrentDictionary TenantLocks = new(); - - /// - public async Task Handle(BindInitialTenantSubscriptionCommand request, CancellationToken cancellationToken) - { - // 1. 校验租户上下文 - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId == 0 || currentTenantId != request.TenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "租户上下文不匹配,请在请求头 X-Tenant-Id 指定目标租户"); - } - - // 1.2 获取租户级幂等锁,避免并发重复创建 - var tenantLock = GetTenantLock(request.TenantId); - await tenantLock.WaitAsync(cancellationToken); - try - { - // 2. 获取租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 3. 幂等校验:若已存在订阅则直接返回 - var existing = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - if (existing is not null) - { - return existing.ToSubscriptionDto() - ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅读取失败"); - } - - // 4. 创建 0 个月订阅(待支付/待生效) - var now = DateTime.UtcNow; - var subscription = new TenantSubscription - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - TenantPackageId = request.TenantPackageId, - EffectiveFrom = now, - EffectiveTo = now, - NextBillingDate = now, - Status = SubscriptionStatus.Pending, - AutoRenew = request.AutoRenew, - Notes = "初次绑定订阅" - }; - - // 5. 记录订阅与历史 - await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken); - await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - TenantSubscriptionId = subscription.Id, - FromPackageId = request.TenantPackageId, - ToPackageId = request.TenantPackageId, - ChangeType = SubscriptionChangeType.New, - EffectiveFrom = now, - EffectiveTo = now, - Amount = null, - Currency = null, - Notes = "初次绑定订阅(0 个月)" - }, cancellationToken); - - // 6. 记录审计日志 - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.SubscriptionUpdated, - Title = "初次绑定订阅", - Description = $"套餐 {request.TenantPackageId},时长 0 月" - }, cancellationToken); - - // 7. 保存变更 - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 8. 返回 DTO - return subscription.ToSubscriptionDto() - ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅绑定失败"); - } - finally - { - // 9. 释放幂等锁 - tenantLock.Release(); - } - } - - // 获取或创建租户级幂等锁实例。 - private static SemaphoreSlim GetTenantLock(long tenantId) - => TenantLocks.GetOrAdd(tenantId, _ => new SemaphoreSlim(1, 1)); -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ChangeTenantSubscriptionPlanCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ChangeTenantSubscriptionPlanCommandHandler.cs deleted file mode 100644 index de73ccf..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ChangeTenantSubscriptionPlanCommandHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 套餐升降配处理器。 -/// -public sealed class ChangeTenantSubscriptionPlanCommandHandler( - ITenantRepository tenantRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(ChangeTenantSubscriptionPlanCommand request, CancellationToken cancellationToken) - { - // 1. 校验租户与订阅存在性 - _ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - var subscription = await tenantRepository.FindSubscriptionByIdAsync(request.TenantId, request.TenantSubscriptionId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "订阅不存在"); - - var previousPackage = subscription.TenantPackageId; - - // 2. 根据立即生效或排期设置目标套餐 - if (request.Immediate) - { - subscription.TenantPackageId = request.TargetPackageId; - subscription.ScheduledPackageId = null; - } - else - { - subscription.ScheduledPackageId = request.TargetPackageId; - } - - // 3. 更新订阅并记录变更历史 - await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken); - await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory - { - Id = idGenerator.NextId(), - TenantId = subscription.TenantId, - TenantSubscriptionId = subscription.Id, - FromPackageId = previousPackage, - ToPackageId = request.TargetPackageId, - ChangeType = SubscriptionChangeType.Upgrade, - EffectiveFrom = subscription.EffectiveFrom, - EffectiveTo = subscription.EffectiveTo, - Notes = request.Notes - }, cancellationToken); - - // 4. 记录审计日志 - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = subscription.TenantId, - Action = TenantAuditAction.SubscriptionPlanChanged, - Title = request.Immediate ? "套餐立即变更" : "套餐排期变更", - Description = request.Notes, - PreviousStatus = null, - CurrentStatus = null - }, cancellationToken); - - // 5. 保存并返回 DTO - await tenantRepository.SaveChangesAsync(cancellationToken); - - return subscription.ToSubscriptionDto() - ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅更新失败"); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs deleted file mode 100644 index 705a32f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs +++ /dev/null @@ -1,92 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 领取租户入驻审核处理器。 -/// -public sealed class ClaimTenantReviewCommandHandler( - ITenantRepository tenantRepository, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) - : IRequestHandler -{ - /// - public async Task Handle(ClaimTenantReviewCommand request, CancellationToken cancellationToken) - { - // 1. 校验租户存在 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2. 查询是否已领取 - var existingClaim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken); - if (existingClaim != null) - { - if (existingClaim.ClaimedBy == currentUserAccessor.UserId) - { - return existingClaim.ToDto(); - } - - throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {existingClaim.ClaimedByName} 领取"); - } - - // 3. 获取当前用户显示名(用于展示快照) - var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var displayName = string.IsNullOrWhiteSpace(profile.DisplayName) - ? $"user:{currentUserAccessor.UserId}" - : profile.DisplayName; - - // 4. 构造领取记录与审计日志 - var now = DateTime.UtcNow; - var claim = new TenantReviewClaim - { - TenantId = request.TenantId, - ClaimedBy = currentUserAccessor.UserId, - ClaimedByName = displayName, - ClaimedAt = now, - ReleasedAt = null - }; - - var auditLog = new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.ReviewClaimed, - Title = "领取审核", - Description = $"领取人:{displayName}", - OperatorId = currentUserAccessor.UserId, - OperatorName = displayName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }; - - // 5. 写入领取记录(处理并发领取冲突) - var success = await tenantRepository.TryAddReviewClaimAsync(claim, auditLog, cancellationToken); - if (!success) - { - var current = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken); - if (current == null) - { - throw new BusinessException(ErrorCodes.Conflict, "审核领取失败,请刷新后重试"); - } - - if (current.ClaimedBy == currentUserAccessor.UserId) - { - return current.ToDto(); - } - - throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {current.ClaimedByName} 领取"); - } - - // 6. 返回领取结果 - return claim.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs deleted file mode 100644 index 788dfa3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs +++ /dev/null @@ -1,94 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 生成租户主管理员重置链接令牌处理器(平台超级管理员使用)。 -/// -public sealed class CreateTenantAdminResetLinkTokenCommandHandler( - ITenantRepository tenantRepository, - ITenantProvider tenantProvider, - ITenantContextAccessor tenantContextAccessor, - IIdentityUserRepository identityUserRepository, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService, - IAdminPasswordResetTokenStore tokenStore) - : IRequestHandler -{ - private const long PlatformRootTenantId = 1000000000001; - - /// - public async Task Handle(CreateTenantAdminResetLinkTokenCommand request, CancellationToken cancellationToken) - { - // 1. 校验仅允许平台超级管理员执行 - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可生成重置链接"); - } - - // 2. 校验租户存在且存在主管理员 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2.1 若缺少主管理员则自动回填(兼容历史数据) - if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0) - { - var originalContextForFix = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "admin:reset-link:fix-owner"); - try - { - var users = await identityUserRepository.SearchAsync(tenant.Id, keyword: null, cancellationToken); - var ownerCandidate = users.OrderBy(x => x.CreatedAt).FirstOrDefault(); - if (ownerCandidate == null) - { - throw new BusinessException(ErrorCodes.BadRequest, "该租户未配置主管理员账号,且未找到可用管理员账号"); - } - - tenant.PrimaryOwnerUserId = ownerCandidate.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - } - finally - { - tenantContextAccessor.Current = originalContextForFix; - } - } - - // 3. 签发一次性重置令牌(默认 24 小时有效) - var token = await tokenStore.IssueAsync(tenant.PrimaryOwnerUserId.Value, DateTime.UtcNow.AddHours(24), cancellationToken); - - // 4. 写入审计日志 - var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) - ? $"user:{currentUserAccessor.UserId}" - : operatorProfile.DisplayName; - - var auditLog = new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.AdminResetLinkIssued, - Title = "生成重置链接", - Description = $"操作者:{operatorName},目标用户ID:{tenant.PrimaryOwnerUserId.Value}", - OperatorId = currentUserAccessor.UserId, - OperatorName = operatorName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }; - await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 5. 返回令牌 - return token; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs index 83ae3a1..3521ffc 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs @@ -7,6 +7,7 @@ using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -15,6 +16,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// public sealed class CreateTenantAnnouncementCommandHandler( ITenantAnnouncementRepository announcementRepository, + ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor) : IRequestHandler { @@ -26,12 +28,31 @@ public sealed class CreateTenantAnnouncementCommandHandler( /// 公告 DTO。 public async Task Handle(CreateTenantAnnouncementCommand request, CancellationToken cancellationToken) { - // 1. 校验标题与内容 + // 1. 校验租户上下文(租户端禁止跨租户/平台公告) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权创建其他租户公告"); + } + + // 3. (空行后) 校验标题与内容 if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content)) { throw new BusinessException(ErrorCodes.ValidationFailed, "公告标题和内容不能为空"); } + // 4. (空行后) 校验公告发布范围:租户端仅允许租户公告 + if (request.PublisherScope != PublisherScope.Tenant) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "租户端不允许创建平台公告"); + } + if (string.IsNullOrWhiteSpace(request.TargetType)) { throw new BusinessException(ErrorCodes.ValidationFailed, "目标受众类型不能为空"); @@ -42,13 +63,8 @@ public sealed class CreateTenantAnnouncementCommandHandler( throw new BusinessException(ErrorCodes.ValidationFailed, "生效开始时间必须早于结束时间"); } - if (request.TenantId == 0 && request.PublisherScope != PublisherScope.Platform) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId=0 仅允许平台公告"); - } - - // 2. 构建公告实体 - var tenantId = request.PublisherScope == PublisherScope.Platform ? 0 : request.TenantId; + // 5. (空行后) 构建公告实体 + var tenantId = currentTenantId; var publisherUserId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId; var announcement = new TenantAnnouncement { @@ -66,7 +82,7 @@ public sealed class CreateTenantAnnouncementCommandHandler( TargetParameters = request.TargetParameters }; - // 3. 持久化并返回 DTO + // 6. (空行后) 持久化并返回 DTO await announcementRepository.AddAsync(announcement, cancellationToken); await announcementRepository.SaveChangesAsync(cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantBillingCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantBillingCommandHandler.cs deleted file mode 100644 index 3819e63..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantBillingCommandHandler.cs +++ /dev/null @@ -1,52 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 创建租户账单处理器。 -/// -public sealed class CreateTenantBillingCommandHandler(ITenantBillingRepository billingRepository) - : IRequestHandler -{ - /// - /// 处理创建租户账单请求。 - /// - /// 创建命令。 - /// 取消标记。 - /// 账单 DTO。 - public async Task Handle(CreateTenantBillingCommand request, CancellationToken cancellationToken) - { - // 1. 校验账单编号 - if (string.IsNullOrWhiteSpace(request.StatementNo)) - { - throw new BusinessException(ErrorCodes.BadRequest, "账单编号不能为空"); - } - - // 2. 构建账单实体 - var bill = new TenantBillingStatement - { - TenantId = request.TenantId, - StatementNo = request.StatementNo.Trim(), - PeriodStart = request.PeriodStart, - PeriodEnd = request.PeriodEnd, - AmountDue = request.AmountDue, - AmountPaid = request.AmountPaid, - Status = request.Status, - DueDate = request.DueDate, - LineItemsJson = request.LineItemsJson - }; - - // 3. 持久化账单 - await billingRepository.AddAsync(bill, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - // 4. 返回 DTO - return bill.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs deleted file mode 100644 index 8297b18..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs +++ /dev/null @@ -1,269 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.Identity.Commands; -using TakeoutSaaS.Domain.Identity.Entities; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 后台手动新增租户处理器。 -/// -public sealed class CreateTenantManuallyCommandHandler( - ITenantRepository tenantRepository, - ITenantPackageRepository tenantPackageRepository, - IIdentityUserRepository identityUserRepository, - IRoleRepository roleRepository, - IPasswordHasher passwordHasher, - IIdGenerator idGenerator, - IMediator mediator, - ITenantContextAccessor tenantContextAccessor, - ICurrentUserAccessor currentUserAccessor, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(CreateTenantManuallyCommand request, CancellationToken cancellationToken) - { - // 1. 校验订阅时长 - if (request.DurationMonths <= 0) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "订阅时长必须大于 0"); - } - - // 2. 校验租户编码唯一性 - var normalizedCode = request.Code.Trim(); - if (await tenantRepository.ExistsByCodeAsync(normalizedCode, cancellationToken)) - { - throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {normalizedCode} 已存在"); - } - - // 3. 校验联系人手机号唯一性(仅当填写时) - if (!string.IsNullOrWhiteSpace(request.ContactPhone)) - { - var normalizedPhone = request.ContactPhone.Trim(); - if (await tenantRepository.ExistsByContactPhoneAsync(normalizedPhone, cancellationToken)) - { - throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册"); - } - } - - // 4. 校验管理员账号唯一性 - var normalizedAccount = request.AdminAccount.Trim(); - if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken)) - { - throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在"); - } - - // 5. 校验套餐存在且可用 - var package = await tenantPackageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在"); - if (!package.IsActive) - { - throw new BusinessException(ErrorCodes.BadRequest, "套餐未启用,无法绑定订阅"); - } - - // 6. 计算订阅生效与到期时间(UTC) - var now = DateTime.UtcNow; - var subscriptionEffectiveFrom = request.SubscriptionEffectiveFrom ?? now; - var subscriptionEffectiveTo = subscriptionEffectiveFrom.AddMonths(request.DurationMonths); - - // 7. 构建租户与订阅 - var tenantId = idGenerator.NextId(); - var tenant = new Tenant - { - Id = tenantId, - Code = normalizedCode, - Name = request.Name.Trim(), - ShortName = request.ShortName, - LegalEntityName = request.LegalEntityName, - Industry = request.Industry, - LogoUrl = request.LogoUrl, - CoverImageUrl = request.CoverImageUrl, - Website = request.Website, - Country = request.Country, - Province = request.Province, - City = request.City, - Address = request.Address, - ContactName = request.ContactName, - ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(), - ContactEmail = request.ContactEmail, - Status = request.TenantStatus, - EffectiveFrom = subscriptionEffectiveFrom, - EffectiveTo = subscriptionEffectiveTo, - SuspendedAt = request.SuspendedAt, - SuspensionReason = request.SuspensionReason, - Tags = request.Tags, - Remarks = request.Remarks - }; - - // 8. 构建订阅实体 - var subscription = new TenantSubscription - { - Id = idGenerator.NextId(), - TenantId = tenantId, - TenantPackageId = request.TenantPackageId, - EffectiveFrom = subscriptionEffectiveFrom, - EffectiveTo = subscriptionEffectiveTo, - NextBillingDate = request.NextBillingDate ?? subscriptionEffectiveTo, - Status = request.SubscriptionStatus, - AutoRenew = request.AutoRenew, - ScheduledPackageId = request.ScheduledPackageId, - Notes = request.SubscriptionNotes - }; - - // 9. 构建认证资料(默认直接通过) - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - var verification = new TenantVerificationProfile - { - Id = idGenerator.NextId(), - TenantId = tenantId, - Status = request.VerificationStatus, - BusinessLicenseNumber = request.BusinessLicenseNumber, - BusinessLicenseUrl = request.BusinessLicenseUrl, - LegalPersonName = request.LegalPersonName, - LegalPersonIdNumber = request.LegalPersonIdNumber, - LegalPersonIdFrontUrl = request.LegalPersonIdFrontUrl, - LegalPersonIdBackUrl = request.LegalPersonIdBackUrl, - BankAccountName = request.BankAccountName, - BankAccountNumber = request.BankAccountNumber, - BankName = request.BankName, - AdditionalDataJson = request.AdditionalDataJson, - SubmittedAt = request.SubmittedAt ?? now, - ReviewedAt = request.ReviewedAt ?? now, - ReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - ReviewedByName = string.IsNullOrWhiteSpace(request.ReviewedByName) ? actorName : request.ReviewedByName, - ReviewRemarks = request.ReviewRemarks - }; - - // 10. 写入审计日志与订阅历史 - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenantId, - Action = TenantAuditAction.RegistrationSubmitted, - Title = "后台手动创建", - Description = $"绑定套餐 {request.TenantPackageId},订阅 {request.DurationMonths} 月", - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = null, - CurrentStatus = tenant.Status - }, cancellationToken); - - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenantId, - Action = TenantAuditAction.SubscriptionUpdated, - Title = "订阅初始化", - Description = $"生效:{subscription.EffectiveFrom:yyyy-MM-dd HH:mm:ss},到期:{subscription.EffectiveTo:yyyy-MM-dd HH:mm:ss}", - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = null, - CurrentStatus = tenant.Status - }, cancellationToken); - - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenantId, - Action = TenantAuditAction.VerificationApproved, - Title = "认证已通过", - Description = request.ReviewRemarks, - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = null, - CurrentStatus = tenant.Status - }, cancellationToken); - - await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory - { - TenantId = tenantId, - TenantSubscriptionId = subscription.Id, - FromPackageId = request.TenantPackageId, - ToPackageId = request.TenantPackageId, - ChangeType = SubscriptionChangeType.New, - EffectiveFrom = subscription.EffectiveFrom, - EffectiveTo = subscription.EffectiveTo, - Amount = null, - Currency = null, - Notes = request.SubscriptionNotes - }, cancellationToken); - - // 11. 持久化租户、订阅与认证资料 - await tenantRepository.AddTenantAsync(tenant, cancellationToken); - await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken); - await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 12. 临时切换租户上下文,保证身份与权限写入正确 - var previousContext = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "manual-create"); - try - { - // 13. 创建租户管理员账号 - var adminUser = new IdentityUser - { - TenantId = tenant.Id, - Account = normalizedAccount, - DisplayName = request.AdminDisplayName.Trim(), - PasswordHash = string.Empty, - Phone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(), - Email = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim(), - MerchantId = request.AdminMerchantId, - Avatar = request.AdminAvatar - }; - adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword); - await identityUserRepository.AddAsync(adminUser, cancellationToken); - await identityUserRepository.SaveChangesAsync(cancellationToken); - - // 14. 初始化租户管理员角色模板并绑定角色 - await mediator.Send(new InitializeRoleTemplatesCommand - { - TemplateCodes = new[] { "tenant-admin" } - }, cancellationToken); - - var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenant.Id, cancellationToken); - if (tenantAdminRole != null) - { - await mediator.Send(new AssignUserRolesCommand - { - UserId = adminUser.Id, - RoleIds = new[] { tenantAdminRole.Id } - }, cancellationToken); - } - - // 15. 回写租户所有者账号 - tenant.PrimaryOwnerUserId = adminUser.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - } - finally - { - // 16. 恢复上下文 - tenantContextAccessor.Current = previousContext; - } - - // 17. 返回创建结果 - logger.LogInformation("已后台手动创建租户 {TenantCode}", tenant.Code); - - return new TenantDetailDto - { - Tenant = TenantMapping.ToDto(tenant, subscription, verification), - Verification = verification.ToVerificationDto(), - Subscription = subscription.ToSubscriptionDto(), - Package = package.ToDto() - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantPackageCommandHandler.cs deleted file mode 100644 index bc43211..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantPackageCommandHandler.cs +++ /dev/null @@ -1,56 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 创建租户套餐处理器。 -/// -public sealed class CreateTenantPackageCommandHandler(ITenantPackageRepository packageRepository) - : IRequestHandler -{ - /// - public async Task Handle(CreateTenantPackageCommand request, CancellationToken cancellationToken) - { - // 1. 校验套餐名称 - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new BusinessException(ErrorCodes.BadRequest, "套餐名称不能为空"); - } - - // 2. 构建套餐实体 - var package = new TenantPackage - { - Name = request.Name.Trim(), - Description = request.Description, - PackageType = request.PackageType, - MonthlyPrice = request.MonthlyPrice, - YearlyPrice = request.YearlyPrice, - MaxStoreCount = request.MaxStoreCount, - MaxAccountCount = request.MaxAccountCount, - MaxStorageGb = request.MaxStorageGb, - MaxSmsCredits = request.MaxSmsCredits, - MaxDeliveryOrders = request.MaxDeliveryOrders, - FeaturePoliciesJson = request.FeaturePoliciesJson, - IsActive = request.IsActive, - IsPublicVisible = request.IsPublicVisible, - IsAllowNewTenantPurchase = request.IsAllowNewTenantPurchase, - PublishStatus = request.PublishStatus ?? TenantPackagePublishStatus.Draft, - IsRecommended = request.IsRecommended, - Tags = request.Tags ?? [], - SortOrder = request.SortOrder - }; - - // 3. 持久化并返回 - await packageRepository.AddAsync(package, cancellationToken); - await packageRepository.SaveChangesAsync(cancellationToken); - - return package.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantSubscriptionCommandHandler.cs deleted file mode 100644 index 4d59ffe..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantSubscriptionCommandHandler.cs +++ /dev/null @@ -1,86 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 新建/续费订阅处理器。 -/// -public sealed class CreateTenantSubscriptionCommandHandler( - ITenantRepository tenantRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(CreateTenantSubscriptionCommand request, CancellationToken cancellationToken) - { - // 1. 校验订阅时长 - if (request.DurationMonths <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "订阅时长必须大于 0"); - } - - // 2. 获取租户与当前订阅 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - var current = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - var from = current?.EffectiveTo ?? tenant.EffectiveTo ?? DateTime.UtcNow; - var effectiveFrom = from > DateTime.UtcNow ? from : DateTime.UtcNow; - var effectiveTo = effectiveFrom.AddMonths(request.DurationMonths); - - // 3. 创建订阅实体 - var subscription = new TenantSubscription - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - TenantPackageId = request.TenantPackageId, - EffectiveFrom = effectiveFrom, - EffectiveTo = effectiveTo, - NextBillingDate = effectiveTo, - Status = SubscriptionStatus.Active, - AutoRenew = request.AutoRenew, - Notes = request.Notes - }; - - // 4. 记录订阅与历史 - await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken); - await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - TenantSubscriptionId = subscription.Id, - FromPackageId = current?.TenantPackageId ?? request.TenantPackageId, - ToPackageId = request.TenantPackageId, - ChangeType = current == null ? SubscriptionChangeType.New : SubscriptionChangeType.Renew, - EffectiveFrom = effectiveFrom, - EffectiveTo = effectiveTo, - Amount = null, - Currency = null, - Notes = request.Notes - }, cancellationToken); - - // 5. 记录审计 - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.SubscriptionUpdated, - Title = current == null ? "创建订阅" : "续费订阅", - Description = $"套餐 {request.TenantPackageId} 时长 {request.DurationMonths} 月" - }, cancellationToken); - - // 6. 保存变更 - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 7. 返回 DTO - return subscription.ToSubscriptionDto() - ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅生成失败"); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs index ca38d89..e175625 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs @@ -1,13 +1,18 @@ using MediatR; using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 删除公告处理器。 /// -public sealed class DeleteTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository) +public sealed class DeleteTenantAnnouncementCommandHandler( + ITenantAnnouncementRepository announcementRepository, + ITenantProvider tenantProvider) : IRequestHandler { /// @@ -18,11 +23,24 @@ public sealed class DeleteTenantAnnouncementCommandHandler(ITenantAnnouncementRe /// 执行结果。 public async Task Handle(DeleteTenantAnnouncementCommand request, CancellationToken cancellationToken) { - // 1. 删除公告 - await announcementRepository.DeleteAsync(request.TenantId, request.AnnouncementId, cancellationToken); + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权删除其他租户公告"); + } + + // 3. (空行后) 删除公告 + await announcementRepository.DeleteAsync(currentTenantId, request.AnnouncementId, cancellationToken); await announcementRepository.SaveChangesAsync(cancellationToken); - // 2. 返回执行结果 + // 4. (空行后) 返回执行结果 return true; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantPackageCommandHandler.cs deleted file mode 100644 index c0f6fb4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantPackageCommandHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 删除租户套餐处理器。 -/// -public sealed class DeleteTenantPackageCommandHandler(ITenantPackageRepository packageRepository) - : IRequestHandler -{ - /// - public async Task Handle(DeleteTenantPackageCommand request, CancellationToken cancellationToken) - { - // 1. 删除套餐 - await packageRepository.DeleteAsync(request.TenantPackageId, cancellationToken); - await packageRepository.SaveChangesAsync(cancellationToken); - - // 2. 返回执行结果 - return true; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ExtendTenantSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ExtendTenantSubscriptionCommandHandler.cs deleted file mode 100644 index 0bcb1d7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ExtendTenantSubscriptionCommandHandler.cs +++ /dev/null @@ -1,108 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 延期/赠送订阅处理器(按当前订阅套餐续费)。 -/// -public sealed class ExtendTenantSubscriptionCommandHandler( - ITenantRepository tenantRepository, - IIdGenerator idGenerator, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(ExtendTenantSubscriptionCommand request, CancellationToken cancellationToken) - { - // 1. 校验时长 - if (request.DurationMonths <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "延期/赠送时长必须大于 0"); - } - - // 2. 获取租户与当前订阅 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - var current = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.BadRequest, "订阅不存在,无法延期/赠送"); - - var now = DateTime.UtcNow; - var effectiveFrom = current.EffectiveTo > now ? current.EffectiveTo : now; - var effectiveTo = effectiveFrom.AddMonths(request.DurationMonths); - - var previousStatus = tenant.Status; - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - // 3. 创建续费订阅 - var subscription = new TenantSubscription - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - TenantPackageId = current.TenantPackageId, - EffectiveFrom = effectiveFrom, - EffectiveTo = effectiveTo, - NextBillingDate = effectiveTo, - Status = SubscriptionStatus.Active, - AutoRenew = current.AutoRenew, - Notes = request.Notes - }; - - await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken); - await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - TenantSubscriptionId = subscription.Id, - FromPackageId = current.TenantPackageId, - ToPackageId = current.TenantPackageId, - ChangeType = SubscriptionChangeType.Renew, - EffectiveFrom = effectiveFrom, - EffectiveTo = effectiveTo, - Amount = null, - Currency = null, - Notes = request.Notes - }, cancellationToken); - - // 4. 若租户处于到期状态则恢复为正常(冻结状态需先解冻) - if (tenant.Status == TenantStatus.Expired) - { - tenant.Status = TenantStatus.Active; - } - - tenant.EffectiveFrom = subscription.EffectiveFrom; - tenant.EffectiveTo = subscription.EffectiveTo; - - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - - // 5. 记录审计 - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.SubscriptionUpdated, - Title = "延期/赠送时长", - Description = $"续费 {request.DurationMonths} 月,到期时间:{current.EffectiveTo:yyyy-MM-dd HH:mm:ss} -> {subscription.EffectiveTo:yyyy-MM-dd HH:mm:ss}", - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = previousStatus, - CurrentStatus = tenant.Status - }, cancellationToken); - - // 6. 保存并返回 - await tenantRepository.SaveChangesAsync(cancellationToken); - return subscription.ToSubscriptionDto() - ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅生成失败"); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs deleted file mode 100644 index 90e9cfe..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs +++ /dev/null @@ -1,106 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 强制接管租户入驻审核处理器。 -/// -public sealed class ForceClaimTenantReviewCommandHandler( - ITenantRepository tenantRepository, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) - : IRequestHandler -{ - /// - public async Task Handle(ForceClaimTenantReviewCommand request, CancellationToken cancellationToken) - { - // 1. 校验租户存在 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2. 获取当前用户显示名(用于展示快照) - var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var displayName = string.IsNullOrWhiteSpace(profile.DisplayName) - ? $"user:{currentUserAccessor.UserId}" - : profile.DisplayName; - - // 3. 读取当前领取记录(可跟踪用于更新) - var claim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken); - if (claim == null) - { - // 4. 未领取则直接创建(记录强制接管动作) - var now = DateTime.UtcNow; - var created = new TenantReviewClaim - { - TenantId = request.TenantId, - ClaimedBy = currentUserAccessor.UserId, - ClaimedByName = displayName, - ClaimedAt = now, - ReleasedAt = null - }; - - var auditLog = new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.ReviewForceClaimed, - Title = "强制接管审核", - Description = $"接管人:{displayName}", - OperatorId = currentUserAccessor.UserId, - OperatorName = displayName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }; - - var success = await tenantRepository.TryAddReviewClaimAsync(created, auditLog, cancellationToken); - if (!success) - { - var current = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken); - if (current != null) - { - return current.ToDto(); - } - - throw new BusinessException(ErrorCodes.Conflict, "审核接管失败,请刷新后重试"); - } - - return created.ToDto(); - } - - // 5. 已由自己领取则直接返回 - if (claim.ClaimedBy == currentUserAccessor.UserId) - { - return claim.ToDto(); - } - - // 6. 更新领取人并记录审计 - var previousOwner = claim.ClaimedByName; - claim.ClaimedBy = currentUserAccessor.UserId; - claim.ClaimedByName = displayName; - claim.ClaimedAt = DateTime.UtcNow; - - await tenantRepository.UpdateReviewClaimAsync(claim, cancellationToken); - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.ReviewForceClaimed, - Title = "强制接管审核", - Description = $"原领取人:{previousOwner},接管人:{displayName}", - OperatorId = currentUserAccessor.UserId, - OperatorName = displayName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }, cancellationToken); - - await tenantRepository.SaveChangesAsync(cancellationToken); - return claim.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/FreezeTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/FreezeTenantCommandHandler.cs deleted file mode 100644 index b48bbae..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/FreezeTenantCommandHandler.cs +++ /dev/null @@ -1,78 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 冻结租户处理器。 -/// -public sealed class FreezeTenantCommandHandler( - ITenantRepository tenantRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(FreezeTenantCommand request, CancellationToken cancellationToken) - { - // 1. 获取租户与订阅 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken); - - var previousStatus = tenant.Status; - if (tenant.Status == TenantStatus.Closed) - { - throw new BusinessException(ErrorCodes.BadRequest, "已注销租户不可冻结"); - } - - if (tenant.Status == TenantStatus.Suspended) - { - throw new BusinessException(ErrorCodes.Conflict, "租户已被冻结"); - } - - // 2. 更新租户状态 - tenant.Status = TenantStatus.Suspended; - tenant.SuspendedAt = DateTime.UtcNow; - tenant.SuspensionReason = request.Reason; - - // 3. 同步暂停订阅 - if (subscription != null) - { - subscription.Status = SubscriptionStatus.Suspended; - await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken); - } - - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - - // 4. 记录审计 - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.StatusChanged, - Title = "冻结租户", - Description = request.Reason, - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = previousStatus, - CurrentStatus = tenant.Status - }, cancellationToken); - - // 5. 保存并返回 - await tenantRepository.SaveChangesAsync(cancellationToken); - return TenantMapping.ToDto(tenant, subscription, verification); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs deleted file mode 100644 index 76aafb6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 审核日志查询。 -/// -public sealed class GetTenantAuditLogsQueryHandler(ITenantRepository tenantRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(GetTenantAuditLogsQuery request, CancellationToken cancellationToken) - { - // 1. 查询审核日志 - var logs = await tenantRepository.GetAuditLogsAsync(request.TenantId, cancellationToken); - var total = logs.Count; - - // 2. 分页映射 - var paged = logs - .Skip((request.Page - 1) * request.PageSize) - .Take(request.PageSize) - .Select(TenantMapping.ToDto) - .ToList(); - - // 3. 返回分页结果 - return new PagedResult(paged, request.Page, request.PageSize, total); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs index 18a6aae..c6b7c50 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs @@ -2,13 +2,18 @@ using MediatR; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 账单详情查询处理器。 /// -public sealed class GetTenantBillQueryHandler(ITenantBillingRepository billingRepository) +public sealed class GetTenantBillQueryHandler( + ITenantBillingRepository billingRepository, + ITenantProvider tenantProvider) : IRequestHandler { /// @@ -19,10 +24,23 @@ public sealed class GetTenantBillQueryHandler(ITenantBillingRepository billingRe /// 账单 DTO 或 null。 public async Task Handle(GetTenantBillQuery request, CancellationToken cancellationToken) { - // 1. 查询账单 - var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken); + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } - // 2. 返回 DTO 或 null + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户账单"); + } + + // 3. (空行后) 查询账单 + var bill = await billingRepository.FindByIdAsync(currentTenantId, request.BillingId, cancellationToken); + + // 4. (空行后) 返回 DTO 或 null return bill?.ToDto(); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs deleted file mode 100644 index 7ae6f83..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 租户详情查询处理器。 -/// -public sealed class GetTenantByIdQueryHandler( - ITenantRepository tenantRepository, - ITenantPackageRepository tenantPackageRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetTenantByIdQuery request, CancellationToken cancellationToken) - { - // 1. 查询租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2. 查询订阅与认证 - var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken); - - // 3. 查询当前套餐 - var package = subscription == null - ? null - : await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken); - - // 4. 组装返回 - return new TenantDetailDto - { - Tenant = TenantMapping.ToDto(tenant, subscription, verification), - Verification = verification.ToVerificationDto(), - Subscription = subscription.ToSubscriptionDto(), - Package = package?.ToDto() - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageTenantsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageTenantsQueryHandler.cs deleted file mode 100644 index f85a5b7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageTenantsQueryHandler.cs +++ /dev/null @@ -1,230 +0,0 @@ -using MediatR; -using System.Data; -using System.Data.Common; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 查询套餐当前使用租户列表处理器。 -/// -public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - public async Task> Handle(GetTenantPackageTenantsQuery request, CancellationToken cancellationToken) - { - // 1. 参数规范化 - var page = request.Page <= 0 ? 1 : request.Page; - var pageSize = request.PageSize <= 0 ? 20 : request.PageSize; - var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim(); - - // 2. 以当前时间为准筛选“有效订阅” - var now = DateTime.UtcNow; - var expiringDays = request.ExpiringWithinDays is > 0 ? request.ExpiringWithinDays : null; - var expiryEnd = expiringDays.HasValue ? now.AddDays(expiringDays.Value) : (DateTime?)null; - var offset = (page - 1) * pageSize; - - // 3. 查询总数 + 列表 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 3.1 统计总数 - var total = await ExecuteScalarIntAsync( - connection, - BuildCountSql(), - [ - ("packageId", request.TenantPackageId), - ("now", now), - ("expiryEnd", expiryEnd), - ("keyword", keyword) - ], - token); - - // 3.2 查询列表 - var listSql = BuildListSql(expiryEnd.HasValue); - await using var listCommand = CreateCommand( - connection, - listSql, - [ - ("packageId", request.TenantPackageId), - ("now", now), - ("expiryEnd", expiryEnd), - ("keyword", keyword), - ("offset", offset), - ("limit", pageSize) - ]); - - await using var reader = await listCommand.ExecuteReaderAsync(token); - var items = new List(); - while (await reader.ReadAsync(token)) - { - items.Add(new TenantPackageTenantDto - { - TenantId = reader.GetInt64(0), - Code = reader.GetString(1), - Name = reader.GetString(2), - Status = (TenantStatus)reader.GetInt32(3), - ContactName = reader.IsDBNull(4) ? null : reader.GetString(4), - ContactPhone = reader.IsDBNull(5) ? null : reader.GetString(5), - SubscriptionEffectiveFrom = reader.GetDateTime(6), - SubscriptionEffectiveTo = reader.GetDateTime(7) - }); - } - - // 3.3 返回分页 - return new PagedResult(items, page, pageSize, total); - }, - cancellationToken); - } - - private static string BuildCountSql() - { - return """ - select count(*) - from public.tenants t - where t."DeletedAt" is null - and ( - @keyword::text is null - or t."Name" ilike ('%' || @keyword::text || '%') - or t."Code" ilike ('%' || @keyword::text || '%') - or coalesce(t."ContactName", '') ilike ('%' || @keyword::text || '%') - or coalesce(t."ContactPhone", '') ilike ('%' || @keyword::text || '%') - ) - and exists ( - select 1 - from public.tenant_subscriptions s - where s."DeletedAt" is null - and s."TenantId" = t."Id" - and s."TenantPackageId" = @packageId - and s."Status" = 1 - and s."EffectiveFrom" <= @now - and s."EffectiveTo" >= @now - and ( - @expiryEnd::timestamp with time zone is null - or s."EffectiveTo" <= @expiryEnd - ) - ); - """; - } - - private static string BuildListSql(bool orderByExpiryAsc) - { - if (orderByExpiryAsc) - { - return """ - select - t."Id" as "TenantId", - t."Code", - t."Name", - t."Status", - t."ContactName", - t."ContactPhone", - s."EffectiveFrom", - s."EffectiveTo" - from public.tenants t - join lateral ( - select s."EffectiveFrom", s."EffectiveTo" - from public.tenant_subscriptions s - where s."DeletedAt" is null - and s."TenantId" = t."Id" - and s."TenantPackageId" = @packageId - and s."Status" = 1 - and s."EffectiveFrom" <= @now - and s."EffectiveTo" >= @now - and ( - @expiryEnd::timestamp with time zone is null - or s."EffectiveTo" <= @expiryEnd - ) - order by s."EffectiveTo" asc - limit 1 - ) s on true - where t."DeletedAt" is null - and ( - @keyword::text is null - or t."Name" ilike ('%' || @keyword::text || '%') - or t."Code" ilike ('%' || @keyword::text || '%') - or coalesce(t."ContactName", '') ilike ('%' || @keyword::text || '%') - or coalesce(t."ContactPhone", '') ilike ('%' || @keyword::text || '%') - ) - order by s."EffectiveTo" asc - offset @offset - limit @limit; - """; - } - - return """ - select - t."Id" as "TenantId", - t."Code", - t."Name", - t."Status", - t."ContactName", - t."ContactPhone", - s."EffectiveFrom", - s."EffectiveTo" - from public.tenants t - join lateral ( - select s."EffectiveFrom", s."EffectiveTo" - from public.tenant_subscriptions s - where s."DeletedAt" is null - and s."TenantId" = t."Id" - and s."TenantPackageId" = @packageId - and s."Status" = 1 - and s."EffectiveFrom" <= @now - and s."EffectiveTo" >= @now - and ( - @expiryEnd::timestamp with time zone is null - or s."EffectiveTo" <= @expiryEnd - ) - order by s."EffectiveTo" desc - limit 1 - ) s on true - where t."DeletedAt" is null - and ( - @keyword::text is null - or t."Name" ilike ('%' || @keyword::text || '%') - or t."Code" ilike ('%' || @keyword::text || '%') - or coalesce(t."ContactName", '') ilike ('%' || @keyword::text || '%') - or coalesce(t."ContactPhone", '') ilike ('%' || @keyword::text || '%') - ) - order by t."CreatedAt" desc - offset @offset - limit @limit; - """; - } - - private static async Task ExecuteScalarIntAsync( - IDbConnection connection, - string sql, - (string Name, object? Value)[] parameters, - CancellationToken cancellationToken) - { - await using var command = CreateCommand(connection, sql, parameters); - var result = await command.ExecuteScalarAsync(cancellationToken); - return result is null or DBNull ? 0 : Convert.ToInt32(result); - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs deleted file mode 100644 index b5e5f18..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs +++ /dev/null @@ -1,153 +0,0 @@ -using MediatR; -using System.Data; -using System.Data.Common; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 查询套餐使用统计处理器。 -/// -public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - public async Task> Handle(GetTenantPackageUsagesQuery request, CancellationToken cancellationToken) - { - // 1. 规范化输入 - var ids = request.TenantPackageIds? - .Where(x => x > 0) - .Distinct() - .ToArray(); - - // 2. 构造 SQL(以当前时间为准统计“有效订阅/使用租户/到期分布”) - var now = DateTime.UtcNow; - var date7 = now.AddDays(7); - var date15 = now.AddDays(15); - var date30 = now.AddDays(30); - var sql = BuildSql(ids, out var parameters, now, date7, date15, date30); - - // 3. 查询统计结果 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - await using var command = CreateCommand(connection, sql, parameters); - await using var reader = await command.ExecuteReaderAsync(token); - var list = new List(); - - // 4. 逐行读取 - while (await reader.ReadAsync(token)) - { - list.Add(new TenantPackageUsageDto - { - TenantPackageId = reader.GetInt64(0), - ActiveSubscriptionCount = reader.GetInt32(1), - ActiveTenantCount = reader.GetInt32(2), - TotalSubscriptionCount = reader.GetInt32(3), - Mrr = reader.IsDBNull(4) ? 0m : reader.GetDecimal(4), - Arr = reader.IsDBNull(5) ? 0m : reader.GetDecimal(5), - ExpiringTenantCount7Days = reader.IsDBNull(6) ? 0 : reader.GetInt32(6), - ExpiringTenantCount15Days = reader.IsDBNull(7) ? 0 : reader.GetInt32(7), - ExpiringTenantCount30Days = reader.IsDBNull(8) ? 0 : reader.GetInt32(8) - }); - } - - return (IReadOnlyList)list; - }, - cancellationToken); - } - - private static string BuildSql( - long[]? ids, - out (string Name, object? Value)[] parameters, - DateTime now, - DateTime date7, - DateTime date15, - DateTime date30) - { - // 1. 基础查询:先按订阅表聚合,再回连套餐表计算 MRR/ARR - var builder = new System.Text.StringBuilder(); - builder.AppendLine(""" - with stats as ( - select - "TenantPackageId" as "TenantPackageId", - count(*) filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveSubscriptionCount", - count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveTenantCount", - count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now and "EffectiveTo" <= @date7) as "ExpiringTenantCount7Days", - count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now and "EffectiveTo" <= @date15) as "ExpiringTenantCount15Days", - count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now and "EffectiveTo" <= @date30) as "ExpiringTenantCount30Days", - count(*) as "TotalSubscriptionCount" - from public.tenant_subscriptions - where "DeletedAt" is null - """); - - var list = new List<(string Name, object? Value)> - { - ("now", now), - ("date7", date7), - ("date15", date15), - ("date30", date30) - }; - - // 2. 可选按套餐 ID 过滤 - if (ids is { Length: > 0 }) - { - builder.Append(" and \"TenantPackageId\" in ("); - for (var i = 0; i < ids.Length; i++) - { - if (i > 0) - { - builder.Append(','); - } - - var name = $"p{i}"; - builder.Append($"@{name}"); - list.Add((name, ids[i])); - } - - builder.AppendLine(")"); - } - - // 3. 分组与回连套餐表 - builder.AppendLine(""" - group by "TenantPackageId" - ) - select - s."TenantPackageId" as "TenantPackageId", - s."ActiveSubscriptionCount" as "ActiveSubscriptionCount", - s."ActiveTenantCount" as "ActiveTenantCount", - s."TotalSubscriptionCount" as "TotalSubscriptionCount", - (s."ActiveSubscriptionCount"::numeric * coalesce(p."MonthlyPrice", (p."YearlyPrice" / 12.0), 0))::numeric(18, 2) as "Mrr", - (s."ActiveSubscriptionCount"::numeric * coalesce(p."YearlyPrice", (p."MonthlyPrice" * 12), 0))::numeric(18, 2) as "Arr", - s."ExpiringTenantCount7Days" as "ExpiringTenantCount7Days", - s."ExpiringTenantCount15Days" as "ExpiringTenantCount15Days", - s."ExpiringTenantCount30Days" as "ExpiringTenantCount30Days" - from stats s - left join public.tenant_packages p on p."Id" = s."TenantPackageId" and p."DeletedAt" is null; - """); - - parameters = list.ToArray(); - return builder.ToString(); - } - - private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - - foreach (var (name, value) in parameters) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - - return (DbCommand)command; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs index 25eaca6..efe1cbe 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs @@ -5,27 +5,43 @@ using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 租户入住进度查询处理器。 /// -public sealed class GetTenantProgressQueryHandler(ITenantRepository tenantRepository) +public sealed class GetTenantProgressQueryHandler( + ITenantRepository tenantRepository, + ITenantProvider tenantProvider) : IRequestHandler { /// public async Task Handle(GetTenantProgressQuery request, CancellationToken cancellationToken) { - // 1. 查询租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户进度"); + } + + // 3. (空行后) 查询租户 + var tenant = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - // 2. 查询订阅与实名 - var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken); + // 4. (空行后) 查询订阅与实名 + var subscription = await tenantRepository.GetActiveSubscriptionAsync(currentTenantId, cancellationToken); + var verification = await tenantRepository.GetVerificationProfileAsync(currentTenantId, cancellationToken); - // 3. 组装进度信息 + // 5. (空行后) 组装进度信息 return new TenantProgressDto { TenantId = tenant.Id, diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs index 57ecd65..4d46550 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs @@ -9,6 +9,7 @@ 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.Tenants.Handlers; @@ -17,22 +18,36 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// public sealed class GetTenantQuotaUsageHistoryQueryHandler( ITenantRepository tenantRepository, + ITenantProvider tenantProvider, IDapperExecutor dapperExecutor) : IRequestHandler> { /// public async Task> Handle(GetTenantQuotaUsageHistoryQuery request, CancellationToken cancellationToken) { - // 1. 校验租户存在 - _ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户配额历史"); + } + + // 3. (空行后) 校验租户存在 + _ = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - // 2. 规范化分页 + // 4. (空行后) 规范化分页 var page = request.Page <= 0 ? 1 : request.Page; var pageSize = request.PageSize is <= 0 or > 100 ? 10 : request.PageSize; var offset = (page - 1) * pageSize; - // 3. 查询总数 + 列表 + // 5. (空行后) 查询总数 + 列表 return await dapperExecutor.QueryAsync( DatabaseConstants.AppDataSource, DatabaseConnectionRole.Read, @@ -43,7 +58,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler( connection, BuildCountSql(), [ - ("tenantId", request.TenantId), + ("tenantId", currentTenantId), ("quotaType", request.QuotaType.HasValue ? (int)request.QuotaType.Value : null), ("startDate", request.StartDate), ("endDate", request.EndDate) @@ -55,7 +70,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler( connection, BuildListSql(), [ - ("tenantId", request.TenantId), + ("tenantId", currentTenantId), ("quotaType", request.QuotaType.HasValue ? (int)request.QuotaType.Value : null), ("startDate", request.StartDate), ("endDate", request.EndDate), diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs deleted file mode 100644 index 4f90a4c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 获取租户审核领取信息查询处理器。 -/// -public sealed class GetTenantReviewClaimQueryHandler(ITenantRepository tenantRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetTenantReviewClaimQuery request, CancellationToken cancellationToken) - { - // 1. 查询当前领取信息(未领取返回 null) - var claim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken); - return claim?.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs deleted file mode 100644 index 55c60ca..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs +++ /dev/null @@ -1,109 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 伪装登录租户处理器(平台超级管理员使用)。 -/// -public sealed class ImpersonateTenantCommandHandler( - ITenantRepository tenantRepository, - ITenantProvider tenantProvider, - ITenantContextAccessor tenantContextAccessor, - IIdentityUserRepository identityUserRepository, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService, - IJwtTokenService jwtTokenService) - : IRequestHandler -{ - private const long PlatformRootTenantId = 1000000000001; - - /// - public async Task Handle(ImpersonateTenantCommand request, CancellationToken cancellationToken) - { - // 1. 校验仅允许平台超级管理员执行 - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可执行伪装登录"); - } - - // 2. 读取操作者信息(在平台租户上下文内) - var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) - ? $"user:{currentUserAccessor.UserId}" - : operatorProfile.DisplayName; - - // 2. 校验租户存在且存在主管理员 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2.1 若缺少主管理员则自动回填(兼容历史数据) - if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0) - { - var originalContextForFix = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "admin:impersonate:fix-owner"); - try - { - var users = await identityUserRepository.SearchAsync(tenant.Id, keyword: null, cancellationToken); - var ownerCandidate = users.OrderBy(x => x.CreatedAt).FirstOrDefault(); - if (ownerCandidate == null) - { - throw new BusinessException(ErrorCodes.BadRequest, "该租户未配置主管理员账号,且未找到可用管理员账号"); - } - - tenant.PrimaryOwnerUserId = ownerCandidate.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - } - finally - { - tenantContextAccessor.Current = originalContextForFix; - } - } - - // 3. 进入目标租户上下文以读取租户内用户(避免多租户查询过滤导致找不到用户) - var originalTenantContext = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, null, "admin:impersonate"); - try - { - // 4. 为租户主管理员签发令牌 - var targetProfile = await adminAuthService.GetProfileAsync(tenant.PrimaryOwnerUserId.Value, cancellationToken); - var token = await jwtTokenService.CreateTokensAsync(targetProfile, false, cancellationToken); - - // 5. 恢复租户上下文后写入审计日志 - tenantContextAccessor.Current = originalTenantContext; - var auditLog = new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.ImpersonatedLogin, - Title = "伪装登录", - Description = $"操作者:{operatorName},目标账号:{targetProfile.Account}", - OperatorId = currentUserAccessor.UserId, - OperatorName = operatorName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }; - await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 6. 返回令牌 - return token; - } - finally - { - // 7. 确保恢复租户上下文 - tenantContextAccessor.Current = originalTenantContext; - } - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs deleted file mode 100644 index 5ad5776..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 标记账单支付处理器。 -/// -public sealed class MarkTenantBillingPaidCommandHandler(ITenantBillingRepository billingRepository) - : IRequestHandler -{ - /// - /// 标记账单支付。 - /// - /// 标记命令。 - /// 取消标记。 - /// 账单 DTO 或 null。 - public async Task Handle(MarkTenantBillingPaidCommand request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken); - if (bill == null) - { - return null; - } - - // 2. 更新支付状态 - bill.AmountPaid = request.AmountPaid; - bill.Status = TenantBillingStatus.Paid; - bill.DueDate = bill.DueDate; - - // 3. 持久化变更 - await billingRepository.UpdateAsync(bill, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - // 4. 返回 DTO - return bill.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs index 6fd9a8f..7d75258 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs @@ -2,13 +2,18 @@ using MediatR; using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 标记通知已读处理器。 /// -public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotificationRepository notificationRepository) +public sealed class MarkTenantNotificationReadCommandHandler( + ITenantNotificationRepository notificationRepository, + ITenantProvider tenantProvider) : IRequestHandler { /// @@ -19,14 +24,27 @@ public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotification /// 通知 DTO 或 null。 public async Task Handle(MarkTenantNotificationReadCommand request, CancellationToken cancellationToken) { - // 1. 查询通知 - var notification = await notificationRepository.FindByIdAsync(request.TenantId, request.NotificationId, cancellationToken); + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权标记其他租户通知"); + } + + // 3. (空行后) 查询通知 + var notification = await notificationRepository.FindByIdAsync(currentTenantId, request.NotificationId, cancellationToken); if (notification == null) { return null; } - // 2. 若未读则标记已读 + // 4. (空行后) 若未读则标记已读 if (notification.ReadAt == null) { notification.ReadAt = DateTime.UtcNow; @@ -34,7 +52,7 @@ public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotification await notificationRepository.SaveChangesAsync(cancellationToken); } - // 3. 返回 DTO + // 5. (空行后) 返回 DTO return notification.ToDto(); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RegisterTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RegisterTenantCommandHandler.cs deleted file mode 100644 index e236ab1..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RegisterTenantCommandHandler.cs +++ /dev/null @@ -1,92 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 租户注册处理器。 -/// -public sealed class RegisterTenantCommandHandler( - ITenantRepository tenantRepository, - IIdGenerator idGenerator, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(RegisterTenantCommand request, CancellationToken cancellationToken) - { - // 1. 校验订阅时长 - if (request.DurationMonths <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "订阅时长必须大于 0"); - } - - // 2. 检查租户编码唯一性 - if (await tenantRepository.ExistsByCodeAsync(request.Code, cancellationToken)) - { - throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {request.Code} 已存在"); - } - - // 3. 计算生效时间 - var now = DateTime.UtcNow; - var effectiveFrom = request.EffectiveFrom ?? now; - var effectiveTo = effectiveFrom.AddMonths(request.DurationMonths); - - // 4. 构建租户实体 - var tenant = new Tenant - { - Id = idGenerator.NextId(), - Code = request.Code.Trim(), - Name = request.Name, - ShortName = request.ShortName, - Industry = request.Industry, - ContactName = request.ContactName, - ContactPhone = request.ContactPhone, - ContactEmail = request.ContactEmail, - Status = TenantStatus.PendingReview, - EffectiveFrom = effectiveFrom, - EffectiveTo = effectiveTo - }; - - // 5. 构建订阅实体 - var subscription = new TenantSubscription - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - TenantPackageId = request.TenantPackageId, - EffectiveFrom = effectiveFrom, - EffectiveTo = effectiveTo, - NextBillingDate = effectiveTo, - Status = SubscriptionStatus.Pending, - AutoRenew = request.AutoRenew, - Notes = "Init subscription" - }; - - // 6. 持久化租户、订阅和审计日志 - await tenantRepository.AddTenantAsync(tenant, cancellationToken); - await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken); - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.RegistrationSubmitted, - Title = "租户注册", - Description = $"提交套餐 {request.TenantPackageId},时长 {request.DurationMonths} 月" - }, cancellationToken); - - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 7. 记录日志 - logger.LogInformation("已注册租户 {TenantCode}", tenant.Code); - - // 8. 返回 DTO - return TenantMapping.ToDto(tenant, subscription, null); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs deleted file mode 100644 index 7124f01..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs +++ /dev/null @@ -1,67 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 释放租户入驻审核领取处理器。 -/// -public sealed class ReleaseTenantReviewClaimCommandHandler( - ITenantRepository tenantRepository, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) - : IRequestHandler -{ - /// - public async Task Handle(ReleaseTenantReviewClaimCommand request, CancellationToken cancellationToken) - { - // 1. 校验租户存在 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2. 查询当前领取记录 - var claim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken); - if (claim == null) - { - return null; - } - - // 3. 非领取人不允许释放(如需接管请使用强制接管) - if (claim.ClaimedBy != currentUserAccessor.UserId) - { - throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {claim.ClaimedByName} 领取"); - } - - // 4. 释放领取并记录审计 - var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var displayName = string.IsNullOrWhiteSpace(profile.DisplayName) - ? $"user:{currentUserAccessor.UserId}" - : profile.DisplayName; - - claim.ReleasedAt = DateTime.UtcNow; - await tenantRepository.UpdateReviewClaimAsync(claim, cancellationToken); - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.ReviewClaimReleased, - Title = "释放审核", - Description = $"释放人:{displayName}", - OperatorId = currentUserAccessor.UserId, - OperatorName = displayName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }, cancellationToken); - - await tenantRepository.SaveChangesAsync(cancellationToken); - return claim.ToDto(); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs deleted file mode 100644 index 7909b76..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs +++ /dev/null @@ -1,226 +0,0 @@ -using MediatR; -using System.Security.Cryptography; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Merchants.Entities; -using TakeoutSaaS.Domain.Merchants.Enums; -using TakeoutSaaS.Domain.Merchants.Repositories; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 租户审核处理器。 -/// -public sealed class ReviewTenantCommandHandler( - ITenantRepository tenantRepository, - IMerchantRepository merchantRepository, - ICurrentUserAccessor currentUserAccessor, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(ReviewTenantCommand request, CancellationToken cancellationToken) - { - // 1. 获取租户与认证资料 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - var reviewClaim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.Conflict, "请先领取审核"); - - if (reviewClaim.ClaimedBy != currentUserAccessor.UserId) - { - throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {reviewClaim.ClaimedByName} 领取"); - } - - var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.BadRequest, "请先提交实名认证资料"); - - var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - - // 2. 记录审核人 - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - // 3. 写入审核信息 - verification.ReviewedAt = DateTime.UtcNow; - verification.ReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId; - verification.ReviewedByName = actorName; - verification.ReviewRemarks = request.Reason; - - var previousStatus = tenant.Status; - - // 4. 更新租户与订阅状态 - if (request.Approve) - { - if (!request.OperatingMode.HasValue) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "审核通过时必须选择经营模式"); - } - - var renewMonths = request.RenewMonths ?? 0; - if (renewMonths <= 0) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "续费时长必须为正整数(月)"); - } - - var now = DateTime.UtcNow; - verification.Status = TenantVerificationStatus.Approved; - tenant.Status = TenantStatus.Active; - tenant.OperatingMode = request.OperatingMode; - if (subscription != null) - { - subscription.Status = SubscriptionStatus.Active; - - if (subscription.EffectiveFrom == default || subscription.EffectiveFrom > now) - { - subscription.EffectiveFrom = now; - } - - var previousEffectiveTo = subscription.EffectiveTo; - var baseEffectiveTo = subscription.EffectiveTo > now ? subscription.EffectiveTo : now; - subscription.EffectiveTo = baseEffectiveTo.AddMonths(renewMonths); - subscription.NextBillingDate = subscription.EffectiveTo; - - await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.SubscriptionUpdated, - Title = "订阅续费", - Description = $"续费 {renewMonths} 月,到期时间:{previousEffectiveTo:yyyy-MM-dd HH:mm:ss} -> {subscription.EffectiveTo:yyyy-MM-dd HH:mm:ss}", - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = previousStatus, - CurrentStatus = tenant.Status - }, cancellationToken); - } - else - { - throw new BusinessException(ErrorCodes.BadRequest, "订阅不存在,无法续费"); - } - - var existingMerchant = await merchantRepository.FindByTenantIdAsync(tenant.Id, cancellationToken); - if (existingMerchant == null) - { - var merchant = new Merchant - { - Id = idGenerator.NextId(), - TenantId = tenant.Id, - BrandName = tenant.Name, - BrandAlias = tenant.ShortName, - Category = tenant.Industry, - ContactPhone = tenant.ContactPhone ?? string.Empty, - ContactEmail = tenant.ContactEmail, - BusinessLicenseNumber = verification.BusinessLicenseNumber, - BusinessLicenseImageUrl = verification.BusinessLicenseUrl, - LegalPerson = verification.LegalPersonName, - Province = tenant.Province, - City = tenant.City, - Address = tenant.Address, - Status = MerchantStatus.Approved, - OperatingMode = request.OperatingMode, - ApprovedAt = now, - ApprovedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - JoinedAt = now, - LastReviewedAt = now, - LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - IsFrozen = false, - RowVersion = RandomNumberGenerator.GetBytes(16) - }; - - await merchantRepository.AddMerchantAsync(merchant, cancellationToken); - await merchantRepository.AddAuditLogAsync(new MerchantAuditLog - { - TenantId = tenant.Id, - MerchantId = merchant.Id, - Action = MerchantAuditAction.ReviewApproved, - Title = "商户审核通过", - Description = request.Reason, - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName - }, cancellationToken); - } - else - { - existingMerchant.Status = MerchantStatus.Approved; - existingMerchant.OperatingMode = request.OperatingMode; - existingMerchant.ApprovedAt = now; - existingMerchant.ApprovedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId; - existingMerchant.LastReviewedAt = now; - existingMerchant.LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId; - existingMerchant.IsFrozen = false; - existingMerchant.FrozenReason = null; - existingMerchant.FrozenAt = null; - await merchantRepository.UpdateMerchantAsync(existingMerchant, cancellationToken); - await merchantRepository.AddAuditLogAsync(new MerchantAuditLog - { - TenantId = tenant.Id, - MerchantId = existingMerchant.Id, - Action = MerchantAuditAction.ReviewApproved, - Title = "商户审核通过", - Description = request.Reason, - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName - }, cancellationToken); - } - } - else - { - verification.Status = TenantVerificationStatus.Rejected; - tenant.Status = TenantStatus.PendingReview; - if (subscription != null) - { - subscription.Status = SubscriptionStatus.Suspended; - } - } - - // 5. 持久化租户与认证资料 - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken); - if (subscription != null) - { - await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken); - } - - // 6. 记录审核日志 - await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog - { - TenantId = tenant.Id, - Action = request.Approve ? TenantAuditAction.VerificationApproved : TenantAuditAction.VerificationRejected, - Title = request.Approve ? "审核通过" : "审核驳回", - Description = request.Reason, - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = previousStatus, - CurrentStatus = tenant.Status - }, cancellationToken); - - // 7. 审核完成自动释放领取 - reviewClaim.ReleasedAt = DateTime.UtcNow; - await tenantRepository.UpdateReviewClaimAsync(reviewClaim, cancellationToken); - await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.ReviewClaimReleased, - Title = "审核完成释放", - Description = $"释放人:{actorName}", - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }, cancellationToken); - - // 8. 保存并返回 DTO - await tenantRepository.SaveChangesAsync(cancellationToken); - await merchantRepository.SaveChangesAsync(cancellationToken); - - return TenantMapping.ToDto(tenant, subscription, verification); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs index d20ebf6..3286117 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs @@ -3,13 +3,18 @@ using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 账单分页查询处理器。 /// -public sealed class SearchTenantBillsQueryHandler(ITenantBillingRepository billingRepository) +public sealed class SearchTenantBillsQueryHandler( + ITenantBillingRepository billingRepository, + ITenantProvider tenantProvider) : IRequestHandler> { /// @@ -20,16 +25,29 @@ public sealed class SearchTenantBillsQueryHandler(ITenantBillingRepository billi /// 分页结果。 public async Task> Handle(SearchTenantBillsQuery request, CancellationToken cancellationToken) { - // 1. 查询账单 - var bills = await billingRepository.SearchAsync(request.TenantId, request.Status, request.From, request.To, cancellationToken); + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } - // 2. 排序与分页 + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户账单"); + } + + // 3. (空行后) 查询账单 + var bills = await billingRepository.SearchAsync(currentTenantId, request.Status, request.From, request.To, cancellationToken); + + // 4. (空行后) 排序与分页 var ordered = bills.OrderByDescending(x => x.PeriodEnd).ToList(); var page = request.Page <= 0 ? 1 : request.Page; var size = request.PageSize <= 0 ? 20 : request.PageSize; var items = ordered.Skip((page - 1) * size).Take(size).Select(x => x.ToDto()).ToList(); - // 3. 返回分页结果 + // 5. (空行后) 返回分页结果 return new PagedResult(items, page, size, ordered.Count); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs index b74ada5..c37a1df 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs @@ -3,13 +3,18 @@ using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 通知分页查询处理器。 /// -public sealed class SearchTenantNotificationsQueryHandler(ITenantNotificationRepository notificationRepository) +public sealed class SearchTenantNotificationsQueryHandler( + ITenantNotificationRepository notificationRepository, + ITenantProvider tenantProvider) : IRequestHandler> { /// @@ -20,22 +25,35 @@ public sealed class SearchTenantNotificationsQueryHandler(ITenantNotificationRep /// 分页结果。 public async Task> Handle(SearchTenantNotificationsQuery request, CancellationToken cancellationToken) { - // 1. 查询通知 + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户通知"); + } + + // 3. (空行后) 查询通知 var notifications = await notificationRepository.SearchAsync( - request.TenantId, + currentTenantId, request.Severity, request.UnreadOnly, null, null, cancellationToken); - // 2. 排序与分页 + // 4. (空行后) 排序与分页 var ordered = notifications.OrderByDescending(x => x.SentAt).ToList(); var page = request.Page <= 0 ? 1 : request.Page; var size = request.PageSize <= 0 ? 20 : request.PageSize; var items = ordered.Skip((page - 1) * size).Take(size).Select(x => x.ToDto()).ToList(); - // 3. 返回分页结果 + // 5. (空行后) 返回分页结果 return new PagedResult(items, page, size, ordered.Count); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantPackagesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantPackagesQueryHandler.cs deleted file mode 100644 index 4081c91..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantPackagesQueryHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 套餐分页查询处理器。 -/// -public sealed class SearchTenantPackagesQueryHandler(ITenantPackageRepository packageRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(SearchTenantPackagesQuery request, CancellationToken cancellationToken) - { - // 1. 查询套餐 - var packages = await packageRepository.SearchAsync(request.Keyword, request.IsActive, cancellationToken); - - // 2. 排序与分页 - var ordered = packages - .OrderBy(x => x.SortOrder) - .ThenByDescending(x => x.CreatedAt) - .ToList(); - var pageIndex = request.Page <= 0 ? 1 : request.Page; - var size = request.PageSize <= 0 ? 20 : request.PageSize; - var pagedItems = ordered - .Skip((pageIndex - 1) * size) - .Take(size) - .Select(x => x.ToDto()) - .ToList(); - - // 3. 返回分页结果 - return new PagedResult(pagedItems, pageIndex, size, ordered.Count); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantsQueryHandler.cs deleted file mode 100644 index cba1dfe..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantsQueryHandler.cs +++ /dev/null @@ -1,59 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 租户分页查询处理器。 -/// -public sealed class SearchTenantsQueryHandler(ITenantRepository tenantRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(SearchTenantsQuery request, CancellationToken cancellationToken) - { - // 1. 按条件分页查询租户 - var (tenants, total) = await tenantRepository.SearchPagedAsync( - request.Status, - request.VerificationStatus, - request.Name, - request.ContactName, - request.ContactPhone, - request.Keyword, - request.Page, - request.PageSize, - cancellationToken); - - // 2. 无数据直接返回 - if (tenants.Count == 0) - { - return new PagedResult([], request.Page, request.PageSize, total); - } - - // 3. 批量查询订阅与实名资料(避免 N+1) - var tenantIds = tenants.Select(x => x.Id).ToArray(); - var subscriptions = await tenantRepository.GetSubscriptionsAsync(tenantIds, cancellationToken); - var verifications = await tenantRepository.GetVerificationProfilesAsync(tenantIds, cancellationToken); - - // 4. 构建订阅与实名资料映射 - var subscriptionByTenantId = subscriptions - .GroupBy(x => x.TenantId) - .ToDictionary(x => x.Key, x => x.FirstOrDefault()); - var verificationByTenantId = verifications.ToDictionary(x => x.TenantId); - - // 5. 映射 DTO(带订阅与认证) - var result = new List(tenants.Count); - foreach (var tenant in tenants) - { - subscriptionByTenantId.TryGetValue(tenant.Id, out var subscription); - verificationByTenantId.TryGetValue(tenant.Id, out var verification); - result.Add(TenantMapping.ToDto(tenant, subscription, verification)); - } - - // 6. 返回分页结果 - return new PagedResult(result, request.Page, request.PageSize, total); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs index 8771c72..93802db 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs @@ -7,6 +7,7 @@ using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Ids; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -15,21 +16,35 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// public sealed class SubmitTenantVerificationCommandHandler( ITenantRepository tenantRepository, + ITenantProvider tenantProvider, IIdGenerator idGenerator) : IRequestHandler { /// public async Task Handle(SubmitTenantVerificationCommand request, CancellationToken cancellationToken) { - // 1. 获取租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权提交其他租户实名认证资料"); + } + + // 3. (空行后) 获取租户 + var tenant = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - // 2. 读取或初始化实名资料 - var profile = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken) + // 4. (空行后) 读取或初始化实名资料 + var profile = await tenantRepository.GetVerificationProfileAsync(currentTenantId, cancellationToken) ?? new TenantVerificationProfile { Id = idGenerator.NextId(), TenantId = tenant.Id }; - // 3. 填充资料 + // 5. (空行后) 填充资料 profile.BusinessLicenseNumber = request.BusinessLicenseNumber; profile.BusinessLicenseUrl = request.BusinessLicenseUrl; profile.LegalPersonName = request.LegalPersonName; @@ -47,7 +62,7 @@ public sealed class SubmitTenantVerificationCommandHandler( profile.ReviewedBy = null; profile.ReviewedByName = null; - // 4. 保存资料并记录审计 + // 6. (空行后) 保存资料并记录审计 await tenantRepository.UpsertVerificationProfileAsync(profile, cancellationToken); await tenantRepository.AddAuditLogAsync(new TenantAuditLog { @@ -58,7 +73,7 @@ public sealed class SubmitTenantVerificationCommandHandler( }, cancellationToken); await tenantRepository.SaveChangesAsync(cancellationToken); - // 5. 返回 DTO + // 7. (空行后) 返回 DTO return profile.ToVerificationDto() ?? throw new BusinessException(ErrorCodes.InternalServerError, "实名资料保存失败"); } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs deleted file mode 100644 index b51275e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 解冻租户处理器。 -/// -public sealed class UnfreezeTenantCommandHandler( - ITenantRepository tenantRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(UnfreezeTenantCommand request, CancellationToken cancellationToken) - { - // 1. 获取租户与订阅 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken); - - var previousStatus = tenant.Status; - if (tenant.Status != TenantStatus.Suspended) - { - throw new BusinessException(ErrorCodes.BadRequest, "当前租户未处于冻结状态"); - } - - // 2. 计算恢复状态(到期则回到到期状态) - var now = DateTime.UtcNow; - var isExpired = subscription != null && subscription.EffectiveTo <= now; - - tenant.Status = isExpired ? TenantStatus.Expired : TenantStatus.Active; - tenant.SuspendedAt = null; - tenant.SuspensionReason = null; - - // 3. 同步订阅状态 - if (subscription != null) - { - subscription.Status = isExpired ? SubscriptionStatus.GracePeriod : SubscriptionStatus.Active; - await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken); - } - - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - - // 4. 记录审计 - var actorName = currentUserAccessor.IsAuthenticated - ? $"user:{currentUserAccessor.UserId}" - : "system"; - - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.StatusChanged, - Title = "解冻租户", - Description = request.Reason, - OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId, - OperatorName = actorName, - PreviousStatus = previousStatus, - CurrentStatus = tenant.Status - }, cancellationToken); - - // 5. 保存并返回 - await tenantRepository.SaveChangesAsync(cancellationToken); - return TenantMapping.ToDto(tenant, subscription, verification); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs index ed19111..6f34e4e 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs @@ -5,18 +5,34 @@ using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 更新公告处理器。 /// -public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository) +public sealed class UpdateTenantAnnouncementCommandHandler( + ITenantAnnouncementRepository announcementRepository, + ITenantProvider tenantProvider) : IRequestHandler { public async Task Handle(UpdateTenantAnnouncementCommand request, CancellationToken cancellationToken) { - // 1. 校验输入 + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权编辑其他租户公告"); + } + + // 3. (空行后) 校验输入 if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content)) { throw new BusinessException(ErrorCodes.ValidationFailed, "公告标题和内容不能为空"); @@ -27,8 +43,8 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); } - // 2. 查询公告 - var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken); + // 4. (空行后) 查询公告 + var announcement = await announcementRepository.FindByIdAsync(currentTenantId, request.AnnouncementId, cancellationToken); if (announcement == null) { return null; @@ -44,14 +60,14 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe throw new BusinessException(ErrorCodes.Conflict, "仅草稿公告允许编辑"); } - // 3. 更新字段 + // 5. (空行后) 更新字段 announcement.Title = request.Title.Trim(); announcement.Content = request.Content; announcement.TargetType = string.IsNullOrWhiteSpace(request.TargetType) ? announcement.TargetType : request.TargetType.Trim(); announcement.TargetParameters = request.TargetParameters; announcement.RowVersion = request.RowVersion; - // 4. 持久化 + // 6. (空行后) 持久化 try { await announcementRepository.UpdateAsync(announcement, cancellationToken); @@ -62,7 +78,7 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe throw new BusinessException(ErrorCodes.Conflict, "公告已被修改,请刷新后重试"); } - // 5. 返回 DTO + // 7. (空行后) 返回 DTO return announcement.ToDto(false, null); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs index a8c4d94..b4ac9ff 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs @@ -4,6 +4,7 @@ using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -12,46 +13,55 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// public sealed class UpdateTenantCommandHandler( ITenantRepository tenantRepository, + ITenantProvider tenantProvider, ILogger logger) : IRequestHandler { /// public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken) { - // 1. 参数校验 - if (request.TenantId <= 0) + // 1. 校验租户上下文(租户端禁止跨租户) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) { - throw new BusinessException(ErrorCodes.BadRequest, "tenantId 不能为空"); + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); } + // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致 + if (request.TenantId > 0 && request.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户信息"); + } + + // 3. (空行后) 参数校验:租户名称 if (string.IsNullOrWhiteSpace(request.Name)) { throw new BusinessException(ErrorCodes.BadRequest, "租户名称不能为空"); } - // 2. 查询租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) + // 4. (空行后) 查询租户 + var tenant = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - // 3. 校验租户名称唯一性(排除自身) + // 5. (空行后) 校验租户名称唯一性(排除自身) var normalizedName = request.Name.Trim(); - if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: request.TenantId, cancellationToken)) + if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: currentTenantId, cancellationToken)) { throw new BusinessException(ErrorCodes.Conflict, $"租户名称 {normalizedName} 已存在"); } - // 4. 校验联系人手机号唯一性(仅当填写时) + // 6. (空行后) 校验联系人手机号唯一性(仅当填写时) if (!string.IsNullOrWhiteSpace(request.ContactPhone)) { var normalizedPhone = request.ContactPhone.Trim(); var existingTenantId = await tenantRepository.FindTenantIdByContactPhoneAsync(normalizedPhone, cancellationToken); - if (existingTenantId.HasValue && existingTenantId.Value != request.TenantId) + if (existingTenantId.HasValue && existingTenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册"); } } - // 5. 更新基础信息(禁止修改 Code) + // 7. (空行后) 更新基础信息(禁止修改 Code) tenant.Name = normalizedName; tenant.ShortName = string.IsNullOrWhiteSpace(request.ShortName) ? null : request.ShortName.Trim(); tenant.Industry = string.IsNullOrWhiteSpace(request.Industry) ? null : request.Industry.Trim(); @@ -59,14 +69,13 @@ public sealed class UpdateTenantCommandHandler( tenant.ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(); tenant.ContactEmail = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim(); - // 6. 持久化更新 + // 8. (空行后) 持久化更新 await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); await tenantRepository.SaveChangesAsync(cancellationToken); - // 7. 记录日志 + // 9. (空行后) 记录日志 logger.LogInformation("已更新租户基础信息 {TenantId}", tenant.Id); return Unit.Value; } } - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs deleted file mode 100644 index fd167de..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 更新租户套餐处理器。 -/// -public sealed class UpdateTenantPackageCommandHandler(ITenantPackageRepository packageRepository) - : IRequestHandler -{ - /// - public async Task Handle(UpdateTenantPackageCommand request, CancellationToken cancellationToken) - { - // 1. 校验必填项 - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new BusinessException(ErrorCodes.BadRequest, "套餐名称不能为空"); - } - - // 2. 查询套餐 - var package = await packageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken); - if (package == null) - { - return null; - } - - // 3. 更新字段 - package.Name = request.Name.Trim(); - package.Description = request.Description; - package.PackageType = request.PackageType; - package.MonthlyPrice = request.MonthlyPrice; - package.YearlyPrice = request.YearlyPrice; - package.MaxStoreCount = request.MaxStoreCount; - package.MaxAccountCount = request.MaxAccountCount; - package.MaxStorageGb = request.MaxStorageGb; - package.MaxSmsCredits = request.MaxSmsCredits; - package.MaxDeliveryOrders = request.MaxDeliveryOrders; - package.FeaturePoliciesJson = request.FeaturePoliciesJson; - package.IsActive = request.IsActive; - package.IsPublicVisible = request.IsPublicVisible; - package.IsAllowNewTenantPurchase = request.IsAllowNewTenantPurchase; - - // 3. 更新发布状态(若未传则保持不变,避免默认值覆盖) - if (request.PublishStatus.HasValue) - { - package.PublishStatus = request.PublishStatus.Value; - } - - // 4. 更新展示配置(推荐与标签) - package.IsRecommended = request.IsRecommended; - package.Tags = request.Tags ?? []; - package.SortOrder = request.SortOrder; - - // 5. 持久化并返回 - await packageRepository.UpdateAsync(package, cancellationToken); - await packageRepository.SaveChangesAsync(cancellationToken); - - return package.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs deleted file mode 100644 index 10aa010..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 租户审核日志查询。 -/// -public sealed record GetTenantAuditLogsQuery( - long TenantId, - int Page = 1, - int PageSize = 20) : IRequest>; diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs deleted file mode 100644 index 43e226d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 单个租户查询。 -/// -public sealed record GetTenantByIdQuery(long TenantId) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageTenantsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageTenantsQuery.cs deleted file mode 100644 index 3e05511..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageTenantsQuery.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 查询指定套餐当前使用租户列表(按“当前有效订阅”口径)。 -/// -public sealed record GetTenantPackageTenantsQuery : IRequest> -{ - /// - /// 套餐 ID。 - /// - public required long TenantPackageId { get; init; } - - /// - /// 关键词(租户名称/编码/联系人/电话)。 - /// - public string? Keyword { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页大小。 - /// - public int PageSize { get; init; } = 20; - - /// - /// 可选:未来 N 天内到期筛选(按“当前有效订阅”口径,且到期时间在 now ~ now+N 天内)。 - /// - public int? ExpiringWithinDays { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageUsagesQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageUsagesQuery.cs deleted file mode 100644 index 729b149..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageUsagesQuery.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 查询套餐使用统计(订阅关联数量、使用租户数量)。 -/// -public sealed record GetTenantPackageUsagesQuery : IRequest> -{ - /// - /// 需要统计的套餐 ID 列表(为空表示统计全部)。 - /// - public long[]? TenantPackageIds { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs deleted file mode 100644 index 9fa6748..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 获取租户审核领取信息查询。 -/// -public sealed record GetTenantReviewClaimQuery(long TenantId) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantPackagesQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantPackagesQuery.cs deleted file mode 100644 index c81a068..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantPackagesQuery.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 分页查询租户套餐。 -/// -public sealed record SearchTenantPackagesQuery : IRequest> -{ - /// - /// 搜索关键词(名称/描述)。 - /// - public string? Keyword { get; init; } - - /// - /// 是否筛选可售套餐。 - /// - public bool? IsActive { get; init; } - - /// - /// 页码。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantsQuery.cs deleted file mode 100644 index 05f34d9..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantsQuery.cs +++ /dev/null @@ -1,52 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 租户分页查询。 -/// -public sealed record SearchTenantsQuery : IRequest> -{ - /// - /// 租户状态(精确匹配)。 - /// - public TenantStatus? Status { get; init; } - - /// - /// 实名认证状态(精确匹配)。 - /// - public TenantVerificationStatus? VerificationStatus { get; init; } - - /// - /// 租户名称(模糊匹配)。 - /// - public string? Name { get; init; } - - /// - /// 联系人姓名(模糊匹配)。 - /// - public string? ContactName { get; init; } - - /// - /// 联系电话(模糊匹配)。 - /// - public string? ContactPhone { get; init; } - - /// - /// 兼容关键词:按“名称/编码/联系人/电话”做模糊匹配。 - /// - public string? Keyword { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页大小。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs index 0616ea9..32d0d5f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs @@ -9,32 +9,6 @@ namespace TakeoutSaaS.Application.App.Tenants; /// internal static class TenantMapping { - /// - /// 将租户实体与订阅、认证信息映射为租户 DTO。 - /// - /// 租户实体。 - /// 订阅信息。 - /// 认证信息。 - /// 租户 DTO。 - public static TenantDto ToDto(Tenant tenant, TenantSubscription? subscription, TenantVerificationProfile? verification) - => new() - { - Id = tenant.Id, - Code = tenant.Code, - Name = tenant.Name, - ShortName = tenant.ShortName, - ContactName = tenant.ContactName, - ContactPhone = tenant.ContactPhone, - ContactEmail = tenant.ContactEmail, - Status = tenant.Status, - VerificationStatus = verification?.Status ?? Domain.Tenants.Enums.TenantVerificationStatus.Draft, - OperatingMode = tenant.OperatingMode, - CurrentPackageId = subscription?.TenantPackageId, - EffectiveFrom = subscription?.EffectiveFrom ?? tenant.EffectiveFrom, - EffectiveTo = subscription?.EffectiveTo ?? tenant.EffectiveTo, - AutoRenew = subscription?.AutoRenew ?? false - }; - /// /// 将租户认证实体映射为 DTO。 /// @@ -65,60 +39,6 @@ internal static class TenantMapping ReviewedAt = profile.ReviewedAt }; - /// - /// 将订阅实体映射为 DTO。 - /// - /// 订阅实体。 - /// 订阅 DTO 或 null。 - public static TenantSubscriptionDto? ToSubscriptionDto(this TenantSubscription? subscription) - => subscription == null - ? null - : new TenantSubscriptionDto - { - Id = subscription.Id, - TenantId = subscription.TenantId, - TenantPackageId = subscription.TenantPackageId, - Status = subscription.Status, - EffectiveFrom = subscription.EffectiveFrom, - EffectiveTo = subscription.EffectiveTo, - NextBillingDate = subscription.NextBillingDate, - AutoRenew = subscription.AutoRenew - }; - - /// - /// 将审计日志实体映射为 DTO。 - /// - /// 审计日志实体。 - /// 审计日志 DTO。 - public static TenantAuditLogDto ToDto(this TenantAuditLog log) - => new() - { - Id = log.Id, - TenantId = log.TenantId, - Action = log.Action, - Title = log.Title, - Description = log.Description, - OperatorName = log.OperatorName, - PreviousStatus = log.PreviousStatus, - CurrentStatus = log.CurrentStatus, - CreatedAt = log.CreatedAt - }; - - /// - /// 将审核领取实体映射为 DTO。 - /// - /// 领取实体。 - /// 领取 DTO。 - public static TenantReviewClaimDto ToDto(this TenantReviewClaim claim) - => new() - { - Id = claim.Id, - TenantId = claim.TenantId, - ClaimedBy = claim.ClaimedBy, - ClaimedByName = claim.ClaimedByName, - ClaimedAt = claim.ClaimedAt - }; - /// /// 将套餐实体映射为 DTO。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs index 69e04c4..036244c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs @@ -24,9 +24,13 @@ public sealed class CreateAnnouncementCommandValidator : AbstractValidator x.TargetType) .NotEmpty(); - RuleFor(x => x) - .Must(x => x.TenantId != 0 || x.PublisherScope == PublisherScope.Platform) - .WithMessage("TenantId=0 仅允许平台公告"); + RuleFor(x => x.TenantId) + .GreaterThan(0) + .WithMessage("TenantId 必须大于 0"); + + RuleFor(x => x.PublisherScope) + .Equal(PublisherScope.Tenant) + .WithMessage("租户端不允许创建平台公告"); RuleFor(x => x.EffectiveFrom) .LessThan(x => x.EffectiveTo!.Value) diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs deleted file mode 100644 index 84bd0a2..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Commands; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 租户审核命令验证器。 -/// -public sealed class ReviewTenantValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public ReviewTenantValidator() - { - RuleFor(x => x.TenantId).GreaterThan(0); - RuleFor(x => x.Reason) - .NotEmpty() - .When(x => !x.Approve); - RuleFor(x => x.OperatingMode) - .NotNull() - .When(x => x.Approve); - RuleFor(x => x.RenewMonths) - .NotNull() - .GreaterThan(0) - .When(x => x.Approve); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs deleted file mode 100644 index 95ee503..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Queries; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 租户列表查询验证器。 -/// -public sealed class SearchTenantsQueryValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public SearchTenantsQueryValidator() - { - RuleFor(x => x.Page).GreaterThan(0); - RuleFor(x => x.PageSize).InclusiveBetween(1, 200); - RuleFor(x => x.Keyword).MaximumLength(128); - RuleFor(x => x.Name).MaximumLength(128); - RuleFor(x => x.ContactName).MaximumLength(64); - RuleFor(x => x.ContactPhone).MaximumLength(32); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs index 9abd854..8bbae52 100644 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs +++ b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs @@ -62,7 +62,7 @@ public sealed class CreateAnnouncementCommandValidatorTests } [Fact] - public void GivenTenantIdZeroAndNotPlatform_WhenValidate_ThenShouldHaveError() + public void GivenTenantIdZero_WhenValidate_ThenShouldHaveError() { // Arrange var command = AnnouncementTestData.CreateValidCreateCommand() with @@ -75,7 +75,23 @@ public sealed class CreateAnnouncementCommandValidatorTests var result = _validator.TestValidate(command); // Assert - result.ShouldHaveValidationErrorFor(x => x); + result.ShouldHaveValidationErrorFor(x => x.TenantId); + } + + [Fact] + public void GivenPlatformPublisherScope_WhenValidate_ThenShouldHaveError() + { + // Arrange + var command = AnnouncementTestData.CreateValidCreateCommand() with + { + PublisherScope = PublisherScope.Platform + }; + + // Act + var result = _validator.TestValidate(command); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.PublisherScope); } [Fact] diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs index f370a4b..4275fec 100644 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs @@ -56,7 +56,8 @@ public sealed class AnnouncementRegressionTests context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); - var handler = new UpdateTenantAnnouncementCommandHandler(repository); + var tenantProvider = new TestTenantProvider(700); + var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider); var command = new UpdateTenantAnnouncementCommand { diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs index 29c793a..b46cc7e 100644 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs @@ -137,7 +137,8 @@ public sealed class AnnouncementWorkflowTests context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); - var handler = new UpdateTenantAnnouncementCommandHandler(repository); + var tenantProvider = new TestTenantProvider(400); + var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider); var command = new UpdateTenantAnnouncementCommand { @@ -175,7 +176,8 @@ public sealed class AnnouncementWorkflowTests context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); - var handler = new UpdateTenantAnnouncementCommandHandler(repository); + var tenantProvider = new TestTenantProvider(500); + var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider); var command = new UpdateTenantAnnouncementCommand {