From 83a4eb0831cd57ba8a18399614882dc39f03f63e Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Fri, 30 Jan 2026 02:32:01 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E4=BE=A7=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/BillingsController.cs | 303 ---------- .../PlatformAnnouncementsController.cs | 261 --------- .../Controllers/QuotaPackagesController.cs | 198 ------- .../Controllers/StatisticsController.cs | 96 --- .../Controllers/SubscriptionsController.cs | 216 ------- .../TenantAnnouncementsController.cs | 332 ----------- .../Controllers/TenantPackagesController.cs | 177 ------ .../Controllers/TenantsController.cs | 395 ------------- .../App/Billings/BillingMapping.cs | 200 ------- .../Commands/BatchUpdateStatusCommand.cs | 25 - .../Billings/Commands/CancelBillingCommand.cs | 19 - .../Commands/ConfirmPaymentCommand.cs | 42 -- .../Billings/Commands/CreateBillCommand.cs | 30 - .../Billings/Commands/CreateBillingCommand.cs | 41 -- .../GenerateSubscriptionBillingCommand.cs | 15 - .../Commands/ProcessOverdueBillingsCommand.cs | 10 - .../Billings/Commands/RecordPaymentCommand.cs | 41 -- .../Commands/UpdateBillStatusCommand.cs | 26 - .../Commands/UpdateBillingStatusCommand.cs | 25 - .../Billings/Commands/VerifyPaymentCommand.cs | 28 - .../App/Billings/Dto/BillDetailDto.cs | 78 --- .../App/Billings/Dto/BillDto.cs | 68 --- .../App/Billings/Dto/BillingDetailDto.cs | 146 ----- .../App/Billings/Dto/BillingDtos.cs | 545 ------------------ .../App/Billings/Dto/BillingExportDto.cs | 104 ---- .../App/Billings/Dto/BillingLineItemDto.cs | 37 -- .../App/Billings/Dto/BillingListDto.cs | 114 ---- .../App/Billings/Dto/BillingStatisticsDto.cs | 91 --- .../App/Billings/Dto/BillingTrendPointDto.cs | 27 - .../App/Billings/Dto/PaymentDto.cs | 63 -- .../App/Billings/Dto/PaymentRecordDto.cs | 95 --- .../BatchUpdateStatusCommandHandler.cs | 88 --- .../Handlers/CancelBillingCommandHandler.cs | 36 -- .../Handlers/ConfirmPaymentCommandHandler.cs | 99 ---- .../Handlers/CreateBillCommandHandler.cs | 61 -- .../Handlers/CreateBillingCommandHandler.cs | 65 --- .../Handlers/ExportBillingsQueryHandler.cs | 44 -- ...nerateSubscriptionBillingCommandHandler.cs | 102 ---- .../Handlers/GetBillDetailQueryHandler.cs | 41 -- .../Handlers/GetBillListQueryHandler.cs | 55 -- .../Handlers/GetBillingDetailQueryHandler.cs | 236 -------- .../Handlers/GetBillingListQueryHandler.cs | 233 -------- .../GetBillingPaymentsQueryHandler.cs | 139 ----- .../GetBillingStatisticsQueryHandler.cs | 187 ------ .../GetOverdueBillingsQueryHandler.cs | 172 ------ .../Handlers/GetTenantPaymentsQueryHandler.cs | 28 - .../ProcessOverdueBillingsCommandHandler.cs | 20 - .../Handlers/RecordPaymentCommandHandler.cs | 81 --- .../UpdateBillStatusCommandHandler.cs | 50 -- .../UpdateBillingStatusCommandHandler.cs | 54 -- .../Handlers/VerifyPaymentCommandHandler.cs | 73 --- .../Billings/Queries/ExportBillingsQuery.cs | 19 - .../Billings/Queries/GetBillDetailQuery.cs | 15 - .../App/Billings/Queries/GetBillListQuery.cs | 47 -- .../Billings/Queries/GetBillingDetailQuery.cs | 15 - .../Billings/Queries/GetBillingListQuery.cs | 72 --- .../Queries/GetBillingPaymentsQuery.cs | 15 - .../Queries/GetBillingStatisticsQuery.cs | 30 - .../Queries/GetOverdueBillingsQuery.cs | 21 - .../Queries/GetTenantPaymentsQuery.cs | 15 - .../ConfirmPaymentCommandValidator.cs | 50 -- .../CreateBillingCommandValidator.cs | 73 --- .../RecordPaymentCommandValidator.cs | 49 -- .../UpdateBillingStatusCommandValidator.cs | 30 - .../Commands/CreateQuotaPackageCommand.cs | 46 -- .../Commands/DeleteQuotaPackageCommand.cs | 14 - .../Commands/PurchaseQuotaPackageCommand.cs | 30 - .../Commands/UpdateQuotaPackageCommand.cs | 51 -- .../UpdateQuotaPackageStatusCommand.cs | 19 - .../App/QuotaPackages/Dto/QuotaPackageDto.cs | 62 -- .../QuotaPackages/Dto/QuotaPackageListDto.cs | 47 -- .../Dto/TenantQuotaPurchaseDto.cs | 64 -- .../QuotaPackages/Dto/TenantQuotaUsageDto.cs | 47 -- .../CreateQuotaPackageCommandHandler.cs | 54 -- .../DeleteQuotaPackageCommandHandler.cs | 29 - .../GetQuotaPackageListQueryHandler.cs | 41 -- .../GetTenantQuotaPurchasesQueryHandler.cs | 43 -- .../GetTenantQuotaUsageQueryHandler.cs | 35 -- .../PurchaseQuotaPackageCommandHandler.cs | 88 --- .../UpdateQuotaPackageCommandHandler.cs | 54 -- .../UpdateQuotaPackageStatusCommandHandler.cs | 34 -- .../Queries/GetQuotaPackageListQuery.cs | 32 - .../Queries/GetTenantQuotaPurchasesQuery.cs | 26 - .../Queries/GetTenantQuotaUsageQuery.cs | 21 - .../Statistics/Dto/ExpiringSubscriptionDto.cs | 49 -- .../Statistics/Dto/QuotaUsageRankingDto.cs | 50 -- .../Statistics/Dto/RevenueStatisticsDto.cs | 53 -- .../Statistics/Dto/SubscriptionOverviewDto.cs | 42 -- .../GetExpiringSubscriptionsQueryHandler.cs | 38 -- .../GetQuotaUsageRankingQueryHandler.cs | 39 -- .../GetRevenueStatisticsQueryHandler.cs | 71 --- .../GetSubscriptionOverviewQueryHandler.cs | 49 -- .../Queries/GetExpiringSubscriptionsQuery.cs | 20 - .../Queries/GetQuotaUsageRankingQuery.cs | 21 - .../Queries/GetRevenueStatisticsQuery.cs | 15 - .../Queries/GetSubscriptionOverviewQuery.cs | 11 - .../BatchExtendSubscriptionsCommand.cs | 72 --- .../Commands/BatchSendReminderCommand.cs | 45 -- .../Commands/ChangeSubscriptionPlanCommand.cs | 34 -- .../Commands/ExtendSubscriptionCommand.cs | 29 - .../Commands/ProcessAutoRenewalCommand.cs | 33 -- .../ProcessRenewalRemindersCommand.cs | 33 -- .../ProcessSubscriptionExpiryCommand.cs | 33 -- .../Commands/UpdateSubscriptionCommand.cs | 28 - .../UpdateSubscriptionStatusCommand.cs | 30 - .../App/Subscriptions/Dto/QuotaUsageDto.cs | 52 -- .../Dto/SubscriptionDetailDto.cs | 106 ---- .../Dto/SubscriptionHistoryDto.cs | 80 --- .../Subscriptions/Dto/SubscriptionListDto.cs | 95 --- .../BatchExtendSubscriptionsCommandHandler.cs | 135 ----- .../BatchSendReminderCommandHandler.cs | 104 ---- .../ChangeSubscriptionPlanCommandHandler.cs | 95 --- .../ExtendSubscriptionCommandHandler.cs | 69 --- .../GetSubscriptionDetailQueryHandler.cs | 140 ----- .../GetSubscriptionListQueryHandler.cs | 62 -- .../ProcessAutoRenewalCommandHandler.cs | 137 ----- .../ProcessRenewalRemindersCommandHandler.cs | 117 ---- ...ProcessSubscriptionExpiryCommandHandler.cs | 63 -- .../UpdateSubscriptionCommandHandler.cs | 48 -- .../UpdateSubscriptionStatusCommandHandler.cs | 46 -- .../Queries/GetSubscriptionDetailQuery.cs | 20 - .../Queries/GetSubscriptionListQuery.cs | 57 -- .../BindInitialTenantSubscriptionCommand.cs | 29 - .../ChangeTenantSubscriptionPlanCommand.cs | 39 -- .../Commands/CheckTenantQuotaCommand.cs | 26 - .../Commands/ClaimTenantReviewCommand.cs | 17 - .../CreateTenantAnnouncementCommand.cs | 67 --- .../Commands/CreateTenantBillingCommand.cs | 56 -- .../Commands/CreateTenantManuallyCommand.cs | 264 --------- .../Commands/CreateTenantPackageCommand.cs | 101 ---- .../CreateTenantSubscriptionCommand.cs | 38 -- .../DeleteTenantAnnouncementCommand.cs | 19 - .../Commands/DeleteTenantPackageCommand.cs | 14 - .../ExtendTenantSubscriptionCommand.cs | 30 - .../Commands/ForceClaimTenantReviewCommand.cs | 17 - .../Tenants/Commands/FreezeTenantCommand.cs | 25 - .../Commands/MarkTenantBillingPaidCommand.cs | 30 - .../MarkTenantNotificationReadCommand.cs | 20 - .../Commands/PublishAnnouncementCommand.cs | 30 - .../Tenants/Commands/RegisterTenantCommand.cs | 71 --- .../ReleaseTenantReviewClaimCommand.cs | 17 - .../Tenants/Commands/ReviewTenantCommand.cs | 38 -- .../Commands/RevokeAnnouncementCommand.cs | 30 - .../SubmitTenantVerificationCommand.cs | 67 --- .../Tenants/Commands/UnfreezeTenantCommand.cs | 24 - .../UpdateTenantAnnouncementCommand.cs | 53 -- .../Tenants/Commands/UpdateTenantCommand.cs | 53 -- .../Commands/UpdateTenantPackageCommand.cs | 106 ---- .../App/Tenants/Dto/QuotaCheckResultDto.cs | 29 - .../App/Tenants/Dto/QuotaUsageHistoryDto.cs | 45 -- .../App/Tenants/Dto/TenantAnnouncementDto.cs | 113 ---- .../App/Tenants/Dto/TenantAuditLogDto.cs | 58 -- .../App/Tenants/Dto/TenantBillingDto.cs | 63 -- .../App/Tenants/Dto/TenantDetailDto.cs | 27 - .../App/Tenants/Dto/TenantDto.cs | 84 --- .../App/Tenants/Dto/TenantNotificationDto.cs | 58 -- .../App/Tenants/Dto/TenantPackageDto.cs | 107 ---- .../App/Tenants/Dto/TenantPackageTenantDto.cs | 50 -- .../App/Tenants/Dto/TenantPackageUsageDto.cs | 52 -- .../App/Tenants/Dto/TenantReviewClaimDto.cs | 38 -- .../App/Tenants/Dto/TenantSubscriptionDto.cs | 54 -- .../App/Tenants/Dto/TenantVerificationDto.cs | 104 ---- ...InitialTenantSubscriptionCommandHandler.cs | 115 ---- ...ngeTenantSubscriptionPlanCommandHandler.cs | 76 --- .../CheckTenantQuotaCommandHandler.cs | 159 ----- .../ClaimTenantReviewCommandHandler.cs | 92 --- .../CreateTenantAnnouncementCommandHandler.cs | 75 --- .../CreateTenantBillingCommandHandler.cs | 52 -- .../CreateTenantManuallyCommandHandler.cs | 258 --------- .../CreateTenantPackageCommandHandler.cs | 56 -- .../CreateTenantSubscriptionCommandHandler.cs | 86 --- .../DeleteTenantAnnouncementCommandHandler.cs | 28 - .../DeleteTenantPackageCommandHandler.cs | 23 - .../ExtendTenantSubscriptionCommandHandler.cs | 108 ---- .../ForceClaimTenantReviewCommandHandler.cs | 106 ---- .../Handlers/FreezeTenantCommandHandler.cs | 78 --- .../GetAnnouncementByIdQueryHandler.cs | 33 -- .../GetTenantAuditLogsQueryHandler.cs | 32 - .../Handlers/GetTenantBillQueryHandler.cs | 28 - .../Handlers/GetTenantByIdQueryHandler.cs | 43 -- .../GetTenantPackageByIdQueryHandler.cs | 23 - .../GetTenantPackageTenantsQueryHandler.cs | 230 -------- .../GetTenantPackageUsagesQueryHandler.cs | 153 ----- .../GetTenantQuotaUsageHistoryQueryHandler.cs | 171 ------ .../GetTenantReviewClaimQueryHandler.cs | 21 - .../GetTenantsAnnouncementsQueryHandler.cs | 78 --- .../MarkTenantBillingPaidCommandHandler.cs | 42 -- ...arkTenantNotificationReadCommandHandler.cs | 40 -- .../PublishAnnouncementCommandHandler.cs | 82 --- .../Handlers/RegisterTenantCommandHandler.cs | 92 --- .../ReleaseTenantReviewClaimCommandHandler.cs | 67 --- .../Handlers/ReviewTenantCommandHandler.cs | 226 -------- .../RevokeAnnouncementCommandHandler.cs | 75 --- .../Handlers/SearchTenantBillsQueryHandler.cs | 35 -- .../SearchTenantNotificationsQueryHandler.cs | 41 -- .../SearchTenantPackagesQueryHandler.cs | 37 -- .../Handlers/SearchTenantsQueryHandler.cs | 59 -- .../SubmitTenantVerificationCommandHandler.cs | 65 --- .../Handlers/UnfreezeTenantCommandHandler.cs | 76 --- .../UpdateTenantAnnouncementCommandHandler.cs | 68 --- .../Handlers/UpdateTenantCommandHandler.cs | 72 --- .../UpdateTenantPackageCommandHandler.cs | 65 --- .../Queries/GetAnnouncementByIdQuery.cs | 20 - .../Queries/GetTenantAuditLogsQuery.cs | 13 - .../App/Tenants/Queries/GetTenantBillQuery.cs | 20 - .../App/Tenants/Queries/GetTenantByIdQuery.cs | 9 - .../Queries/GetTenantPackageByIdQuery.cs | 15 - .../Queries/GetTenantPackageTenantsQuery.cs | 36 -- .../Queries/GetTenantPackageUsagesQuery.cs | 16 - .../GetTenantQuotaUsageHistoryQuery.cs | 43 -- .../Queries/GetTenantReviewClaimQuery.cs | 9 - .../Queries/GetTenantsAnnouncementsQuery.cs | 62 -- .../Tenants/Queries/SearchTenantBillsQuery.cs | 42 -- .../Queries/SearchTenantNotificationsQuery.cs | 37 -- .../Queries/SearchTenantPackagesQuery.cs | 31 - .../App/Tenants/Queries/SearchTenantsQuery.cs | 52 -- .../App/Tenants/TenantMapping.cs | 221 ------- .../CreateAnnouncementCommandValidator.cs | 36 -- ...etTenantQuotaUsageHistoryQueryValidator.cs | 27 - .../PublishAnnouncementCommandValidator.cs | 24 - .../Validators/ReviewTenantValidator.cs | 28 - .../RevokeAnnouncementCommandValidator.cs | 24 - .../Validators/SearchTenantsQueryValidator.cs | 23 - .../UpdateAnnouncementCommandValidator.cs | 28 - .../AppServiceCollectionExtensions.cs | 16 +- .../Repositories/TenantBillingRepository.cs | 378 ------------ .../Repositories/TenantPaymentRepository.cs | 76 --- .../Repositories/EfQuotaPackageRepository.cs | 169 ------ .../Repositories/EfStatisticsRepository.cs | 116 ---- .../Repositories/EfSubscriptionRepository.cs | 412 ------------- .../EfTenantAnnouncementRepository.cs | 124 ---- .../EfTenantNotificationRepository.cs | 94 --- .../Repositories/EfTenantPackageRepository.cs | 89 --- .../EfTenantQuotaUsageHistoryRepository.cs | 24 - .../EfTenantQuotaUsageRepository.cs | 50 -- .../App/Services/BillingDomainService.cs | 202 ------- .../App/Services/BillingExportService.cs | 203 ------- .../BackgroundServices/AutoRenewalService.cs | 176 ------ .../RenewalReminderService.cs | 171 ------ .../SubscriptionExpiryCheckService.cs | 132 ----- .../appsettings.backgroundservices.json | 16 - .../SchedulerServiceCollectionExtensions.cs | 16 +- .../Jobs/BillingOverdueProcessJob.cs | 32 - .../Jobs/SubscriptionAutoRenewalJob.cs | 36 -- .../Jobs/SubscriptionExpiryCheckJob.cs | 35 -- .../Jobs/SubscriptionRenewalReminderJob.cs | 35 -- .../Options/BillingAutomationOptions.cs | 16 - .../Options/SubscriptionAutomationOptions.cs | 46 -- .../Services/RecurringJobRegistrar.cs | 31 +- 249 files changed, 5 insertions(+), 18156 deletions(-) delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/QuotaPackagesController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/StatisticsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/TenantPackagesController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/BatchUpdateStatusCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/CancelBillingCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/ConfirmPaymentCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillingCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/GenerateSubscriptionBillingCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/ProcessOverdueBillingsCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/RecordPaymentCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillStatusCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillingStatusCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Commands/VerifyPaymentCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDetailDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingExportDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingLineItemDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingTrendPointDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/BatchUpdateStatusCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CancelBillingCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ConfirmPaymentCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillingCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ExportBillingsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GenerateSubscriptionBillingCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillDetailQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillListQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingDetailQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingListQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingPaymentsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingStatisticsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetOverdueBillingsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetTenantPaymentsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ProcessOverdueBillingsCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/RecordPaymentCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillStatusCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillingStatusCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Handlers/VerifyPaymentCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/ExportBillingsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillDetailQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillListQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingDetailQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingPaymentsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetOverdueBillingsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetTenantPaymentsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Validators/ConfirmPaymentCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Validators/CreateBillingCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Validators/RecordPaymentCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Validators/UpdateBillingStatusCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/CreateQuotaPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/DeleteQuotaPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/PurchaseQuotaPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageStatusCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageListDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaPurchaseDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaUsageDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/CreateQuotaPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/DeleteQuotaPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetQuotaPackageListQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaPurchasesQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaUsageQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/PurchaseQuotaPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageStatusCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetQuotaPackageListQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaPurchasesQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaUsageQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Dto/ExpiringSubscriptionDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Dto/QuotaUsageRankingDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Dto/RevenueStatisticsDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Dto/SubscriptionOverviewDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetExpiringSubscriptionsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetQuotaUsageRankingQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetRevenueStatisticsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetSubscriptionOverviewQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetExpiringSubscriptionsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetQuotaUsageRankingQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetRevenueStatisticsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetSubscriptionOverviewQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchExtendSubscriptionsCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchSendReminderCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ChangeSubscriptionPlanCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ExtendSubscriptionCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessAutoRenewalCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessRenewalRemindersCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessSubscriptionExpiryCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionStatusCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/QuotaUsageDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionDetailDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionHistoryDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionListDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.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/CheckTenantQuotaCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAnnouncementCommand.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/DeleteTenantAnnouncementCommand.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/MarkTenantBillingPaidCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantNotificationReadCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.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/RevokeAnnouncementCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantAnnouncementCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/QuotaCheckResultDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/QuotaUsageHistoryDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAnnouncementDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantBillingDto.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/TenantNotificationDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageDto.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/Dto/TenantVerificationDto.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/CheckTenantQuotaCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.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/DeleteTenantAnnouncementCommandHandler.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/GetAnnouncementByIdQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageByIdQueryHandler.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/GetTenantQuotaUsageHistoryQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.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/RevokeAnnouncementCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.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/SubmitTenantVerificationCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetAnnouncementByIdQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantBillQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageByIdQuery.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/GetTenantQuotaUsageHistoryQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantsAnnouncementsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantBillsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantNotificationsQuery.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/TenantMapping.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/GetTenantQuotaUsageHistoryQueryValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/PublishAnnouncementCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/RevokeAnnouncementCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/UpdateAnnouncementCommandValidator.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantPaymentRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfQuotaPackageRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStatisticsRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantNotificationRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPackageRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageHistoryRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingDomainService.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingExportService.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/AutoRenewalService.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/RenewalReminderService.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/appsettings.backgroundservices.json delete mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/BillingOverdueProcessJob.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Options/BillingAutomationOptions.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Options/SubscriptionAutomationOptions.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs deleted file mode 100644 index b4aa0e8..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs +++ /dev/null @@ -1,303 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 账单管理。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/billings")] -public sealed class BillingsController(IMediator mediator) : BaseApiController -{ - /// - /// 分页查询账单列表。 - /// - /// 账单分页结果。 - [HttpGet] - [PermissionAuthorize("bill:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetList([FromQuery] GetBillingListQuery query, CancellationToken cancellationToken) - { - // 1. 查询账单列表 - var result = await mediator.Send(query, cancellationToken); - - // 2. 返回分页结果 - return ApiResponse>.Ok(result); - } - - /// - /// 获取账单详情。 - /// - /// 账单 ID。 - /// 取消标记。 - /// 账单详情。 - [HttpGet("{id:long}")] - [PermissionAuthorize("bill:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> GetDetail(long id, CancellationToken cancellationToken) - { - // 1. 查询账单详情(若不存在则抛出业务异常,由全局异常处理转换为 404) - var result = await mediator.Send(new GetBillingDetailQuery { BillingId = id }, cancellationToken); - - // 2. 返回详情 - return ApiResponse.Ok(result); - } - - /// - /// 手动创建账单。 - /// - /// 创建账单命令。 - /// 取消标记。 - /// 创建的账单信息。 - [HttpPost] - [PermissionAuthorize("bill:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Create([FromBody, Required] CreateBillingCommand command, CancellationToken cancellationToken) - { - // 1. 创建账单 - var result = await mediator.Send(command, cancellationToken); - - // 2. 返回创建结果 - return ApiResponse.Ok(result); - } - - /// - /// 更新账单状态。 - /// - /// 账单 ID。 - /// 更新状态命令。 - /// 取消标记。 - /// 更新结果。 - [HttpPut("{id:long}/status")] - [PermissionAuthorize("bill:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> UpdateStatus(long id, [FromBody, Required] UpdateBillingStatusCommand command, CancellationToken cancellationToken) - { - // 1. 绑定账单标识 - command = command with { BillingId = id }; - - // 2. 更新账单状态(若不存在则抛出业务异常,由全局异常处理转换为 404) - await mediator.Send(command, cancellationToken); - - // 3. 返回成功结果 - return ApiResponse.Ok(null); - } - - /// - /// 取消账单。 - /// - /// 账单 ID。 - /// 取消原因(可选)。 - /// 取消标记。 - /// 取消结果。 - [HttpDelete("{id:long}")] - [PermissionAuthorize("bill:delete")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Cancel(long id, [FromQuery] string? reason, CancellationToken cancellationToken) - { - // 1. 取消账单(取消原因支持可选) - await mediator.Send(new CancelBillingCommand { BillingId = id, Reason = reason ?? string.Empty }, cancellationToken); - - // 2. 返回成功结果 - return ApiResponse.Ok(null); - } - - /// - /// 获取账单支付记录。 - /// - /// 账单 ID。 - /// 取消标记。 - /// 支付记录列表。 - [HttpGet("{id:long}/payments")] - [PermissionAuthorize("bill:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetPayments(long id, CancellationToken cancellationToken) - { - // 1. 查询支付记录 - var result = await mediator.Send(new GetBillingPaymentsQuery { BillingId = id }, cancellationToken); - - // 2. 返回列表 - return ApiResponse>.Ok(result); - } - - /// - /// 记录支付(线下支付确认)。 - /// - /// 账单 ID。 - /// 记录支付命令。 - /// 取消标记。 - /// 支付记录信息。 - [HttpPost("{id:long}/payments")] - [PermissionAuthorize("bill:pay")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> RecordPayment(long id, [FromBody, Required] RecordPaymentCommand command, CancellationToken cancellationToken) - { - // 1. 绑定账单标识 - command = command with { BillingId = id }; - - // 2. 记录支付 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回支付记录 - return ApiResponse.Ok(result); - } - - /// - /// 一键确认收款(记录支付 + 立即审核通过)。 - /// - /// 账单 ID。 - /// 确认收款命令。 - /// 取消标记。 - /// 确认后的支付记录。 - [HttpPost("{id:long}/payments/confirm")] - [PermissionAuthorize("bill:pay")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> ConfirmPayment(long id, [FromBody, Required] ConfirmPaymentCommand command, CancellationToken cancellationToken) - { - // 1. 绑定账单标识 - command = command with { BillingId = id }; - - // 2. 一键确认收款(含:写入 VerifiedBy/VerifiedAt,并同步更新账单已收金额/状态) - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回结果 - return ApiResponse.Ok(result); - } - - /// - /// 审核支付记录。 - /// - /// 支付记录 ID。 - /// 审核参数。 - /// 取消标记。 - /// 审核后的支付记录。 - [HttpPut("payments/{paymentId:long}/verify")] - [PermissionAuthorize("bill:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> VerifyPayment(long paymentId, [FromBody, Required] VerifyPaymentCommand command, CancellationToken cancellationToken) - { - // 1. 绑定支付记录标识 - command = command with { PaymentId = paymentId }; - - // 2. 审核支付记录 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回审核结果 - return ApiResponse.Ok(result); - } - - /// - /// 批量更新账单状态。 - /// - /// 批量更新命令。 - /// 取消标记。 - /// 更新条数。 - [HttpPost("batch/status")] - [PermissionAuthorize("bill:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> BatchUpdateStatus([FromBody, Required] BatchUpdateStatusCommand command, CancellationToken cancellationToken) - { - // 1. 执行批量更新 - var affected = await mediator.Send(command, cancellationToken); - - // 2. 返回更新条数 - return ApiResponse.Ok(affected); - } - - /// - /// 导出账单(Excel/PDF/CSV)。 - /// - /// 导出请求。 - /// 取消标记。 - /// 导出文件。 - [HttpPost("export")] - [PermissionAuthorize("bill:read")] - [Produces("application/octet-stream")] - [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] - public async Task Export([FromBody, Required] ExportBillingsQuery query, CancellationToken cancellationToken) - { - // 1. 执行导出 - var bytes = await mediator.Send(query, cancellationToken); - - // 2. 解析格式并生成文件名 - var extension = ResolveExportFileExtension(query.Format); - var fileName = $"billings_{DateTime.UtcNow:yyyyMMdd_HHmmss}.{extension}"; - - // 3. 显式写入 Content-Disposition,确保浏览器以附件形式下载 - Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment") - { - FileName = fileName, - FileNameStar = fileName - }.ToString(); - - // 4. 返回二进制流(统一 octet-stream,避免被默认 JSON Produces 影响) - return File(bytes, "application/octet-stream"); - } - - /// - /// 获取账单统计数据。 - /// - /// 统计查询参数。 - /// 取消标记。 - /// 统计结果。 - [HttpGet("statistics")] - [PermissionAuthorize("bill:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Statistics([FromQuery] GetBillingStatisticsQuery query, CancellationToken cancellationToken) - { - // 1. 查询统计数据 - var result = await mediator.Send(query, cancellationToken); - - // 2. 返回统计结果 - return ApiResponse.Ok(result); - } - - /// - /// 获取逾期账单列表。 - /// - /// 逾期列表查询参数。 - /// 取消标记。 - /// 逾期账单分页结果。 - [HttpGet("overdue")] - [PermissionAuthorize("bill:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Overdue([FromQuery] GetOverdueBillingsQuery query, CancellationToken cancellationToken) - { - // 1. 查询逾期账单分页列表 - var result = await mediator.Send(query, cancellationToken); - - // 2. 返回分页结果 - return ApiResponse>.Ok(result); - } - - private static string ResolveExportFileExtension(string? format) - { - // 1. 归一化导出格式 - var normalized = (format ?? string.Empty).Trim(); - - // 2. 映射扩展名 - return normalized.ToUpperInvariant() switch - { - "PDF" => "pdf", - "CSV" => "csv", - _ => "xlsx" - }; - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs deleted file mode 100644 index 2eb65e5..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs +++ /dev/null @@ -1,261 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Swashbuckle.AspNetCore.Annotations; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 平台公告管理。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/platform/announcements")] -public sealed class PlatformAnnouncementsController(IMediator mediator) : BaseApiController -{ - /// - /// 创建平台公告。 - /// - /// - /// 示例: - /// - /// POST /api/platform/announcements - /// Header: Authorization: Bearer <JWT> - /// Body: - /// { - /// "title": "平台升级通知", - /// "content": "系统将于今晚 23:00 维护。", - /// "announcementType": 0, - /// "priority": 10, - /// "effectiveFrom": "2025-12-20T00:00:00Z", - /// "effectiveTo": null, - /// "targetType": "all", - /// "targetParameters": null - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "tenantId": "0", - /// "title": "平台升级通知", - /// "status": "Draft" - /// } - /// } - /// - /// - [HttpPost] - [PermissionAuthorize("platform-announcement:create")] - [SwaggerOperation(Summary = "创建平台公告", Description = "需要权限:platform-announcement:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Create([FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken) - { - command = command with - { - TenantId = 0, - PublisherScope = PublisherScope.Platform - }; - - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 查询平台公告列表。 - /// - /// - /// 示例: - /// - /// GET /api/platform/announcements?page=1&pageSize=20&status=Published - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "items": [], - /// "page": 1, - /// "pageSize": 20, - /// "totalCount": 0 - /// } - /// } - /// - /// - [HttpGet] - [PermissionAuthorize("platform-announcement:read", "platform-announcement:create")] - [SwaggerOperation(Summary = "查询平台公告列表", Description = "需要权限:platform-announcement:read 或 platform-announcement:create")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task>> List([FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken) - { - var request = query with { TenantId = 0 }; - var result = await mediator.Send(request, cancellationToken); - return ApiResponse>.Ok(result); - } - - /// - /// 获取平台公告详情。 - /// - /// - /// 示例: - /// - /// GET /api/platform/announcements/900123456789012345 - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "tenantId": "0", - /// "title": "平台升级通知", - /// "status": "Draft" - /// } - /// } - /// - /// - [HttpGet("{announcementId:long}")] - [PermissionAuthorize("platform-announcement:read", "platform-announcement:create")] - [SwaggerOperation(Summary = "获取平台公告详情", Description = "需要权限:platform-announcement:read 或 platform-announcement:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Detail(long announcementId, CancellationToken cancellationToken) - { - var result = await mediator.Send(new GetAnnouncementByIdQuery { TenantId = 0, AnnouncementId = announcementId }, cancellationToken); - - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } - - /// - /// 更新平台公告(仅草稿)。 - /// - /// - /// 示例: - /// - /// PUT /api/platform/announcements/900123456789012345 - /// Body: - /// { - /// "title": "平台升级通知(更新)", - /// "content": "维护时间调整为 23:30。", - /// "targetType": "all", - /// "targetParameters": null, - /// "rowVersion": "AAAAAAAAB9E=" - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "status": "Draft" - /// } - /// } - /// - /// - [HttpPut("{announcementId:long}")] - [PermissionAuthorize("platform-announcement:create")] - [SwaggerOperation(Summary = "更新平台公告", Description = "仅草稿可更新;需要权限:platform-announcement:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Update(long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken) - { - command = command with { TenantId = 0, AnnouncementId = announcementId }; - var result = await mediator.Send(command, cancellationToken); - - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } - - /// - /// 发布平台公告。 - /// - /// - /// 示例: - /// - /// POST /api/platform/announcements/900123456789012345/publish - /// Body: - /// { - /// "rowVersion": "AAAAAAAAB9E=" - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "status": "Published" - /// } - /// } - /// - /// - [HttpPost("{announcementId:long}/publish")] - [PermissionAuthorize("platform-announcement:publish")] - [SwaggerOperation(Summary = "发布平台公告", Description = "需要权限:platform-announcement:publish")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Publish(long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken) - { - command = command with { TenantId = 0, AnnouncementId = announcementId }; - var result = await mediator.Send(command, cancellationToken); - - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } - - /// - /// 撤销平台公告。 - /// - /// - /// 示例: - /// - /// POST /api/platform/announcements/900123456789012345/revoke - /// Body: - /// { - /// "rowVersion": "AAAAAAAAB9E=" - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "status": "Revoked" - /// } - /// } - /// - /// - [HttpPost("{announcementId:long}/revoke")] - [PermissionAuthorize("platform-announcement:revoke")] - [SwaggerOperation(Summary = "撤销平台公告", Description = "需要权限:platform-announcement:revoke")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Revoke(long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken) - { - command = command with { TenantId = 0, AnnouncementId = announcementId }; - var result = await mediator.Send(command, cancellationToken); - - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/QuotaPackagesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/QuotaPackagesController.cs deleted file mode 100644 index b6a3ee2..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/QuotaPackagesController.cs +++ /dev/null @@ -1,198 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.QuotaPackages.Commands; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Application.App.QuotaPackages.Queries; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 配额包管理。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/quota-packages")] -public sealed class QuotaPackagesController(IMediator mediator) : BaseApiController -{ - /// - /// 配额包列表。 - /// - /// 查询条件。 - /// 取消标记。 - /// 配额包分页结果。 - [HttpGet] - [PermissionAuthorize("quota-package:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List([FromQuery] GetQuotaPackageListQuery query, CancellationToken cancellationToken) - { - // 1. 查询配额包分页 - var result = await mediator.Send(query, cancellationToken); - - // 2. 返回结果 - return ApiResponse>.Ok(result); - } - - /// - /// 创建配额包。 - /// - /// 创建命令。 - /// 取消标记。 - /// 创建后的配额包。 - [HttpPost] - [PermissionAuthorize("quota-package:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Create([FromBody, Required] CreateQuotaPackageCommand command, CancellationToken cancellationToken) - { - // 1. 执行创建 - var result = await mediator.Send(command, cancellationToken); - - // 2. 返回创建结果 - return ApiResponse.Ok(result); - } - - /// - /// 更新配额包。 - /// - /// 配额包 ID。 - /// 更新命令。 - /// 取消标记。 - /// 更新后的配额包或未找到。 - [HttpPut("{quotaPackageId:long}")] - [PermissionAuthorize("quota-package:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Update(long quotaPackageId, [FromBody, Required] UpdateQuotaPackageCommand command, CancellationToken cancellationToken) - { - // 1. 绑定路由 ID - command = command with { QuotaPackageId = quotaPackageId }; - - // 2. 执行更新 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回更新结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "配额包不存在") - : ApiResponse.Ok(result); - } - - /// - /// 删除配额包。 - /// - /// 配额包 ID。 - /// 取消标记。 - /// 删除结果。 - [HttpDelete("{quotaPackageId:long}")] - [PermissionAuthorize("quota-package:delete")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Delete(long quotaPackageId, CancellationToken cancellationToken) - { - // 1. 构建删除命令 - var command = new DeleteQuotaPackageCommand { QuotaPackageId = quotaPackageId }; - - // 2. 执行删除并返回 - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 上架/下架配额包。 - /// - /// 配额包 ID。 - /// 状态更新命令。 - /// 取消标记。 - /// 更新结果。 - [HttpPut("{quotaPackageId:long}/status")] - [PermissionAuthorize("quota-package:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> UpdateStatus(long quotaPackageId, [FromBody, Required] UpdateQuotaPackageStatusCommand command, CancellationToken cancellationToken) - { - // 1. 绑定路由 ID - command = command with { QuotaPackageId = quotaPackageId }; - - // 2. 执行状态更新 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回结果 - return ApiResponse.Ok(result); - } - - /// - /// 为租户购买配额包。 - /// - /// 租户 ID。 - /// 购买命令。 - /// 取消标记。 - /// 购买记录。 - [HttpPost("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-packages")] - [PermissionAuthorize("tenant:quota:purchase")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> PurchaseForTenant( - long tenantId, - [FromBody, Required] PurchaseQuotaPackageCommand command, - CancellationToken cancellationToken) - { - // 1. 绑定租户 ID - command = command with { TenantId = tenantId }; - - // 2. 执行购买 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回购买结果 - return ApiResponse.Ok(result); - } - - /// - /// 租户配额使用情况。 - /// - /// 租户 ID。 - /// 查询条件。 - /// 取消标记。 - /// 配额使用情况列表。 - [HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-usage")] - [PermissionAuthorize("tenant:quota:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetTenantQuotaUsage( - long tenantId, - [FromQuery] GetTenantQuotaUsageQuery query, - CancellationToken cancellationToken) - { - // 1. 绑定租户 ID - query = query with { TenantId = tenantId }; - - // 2. 查询配额使用情况 - var result = await mediator.Send(query, cancellationToken); - - // 3. 返回结果 - return ApiResponse>.Ok(result); - } - - /// - /// 租户配额购买记录。 - /// - /// 租户 ID。 - /// 查询条件。 - /// 取消标记。 - /// 购买记录分页结果。 - [HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-purchases")] - [PermissionAuthorize("tenant:quota:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetTenantQuotaPurchases( - long tenantId, - [FromQuery] GetTenantQuotaPurchasesQuery query, - CancellationToken cancellationToken) - { - // 1. 绑定租户 ID - query = query with { TenantId = tenantId }; - - // 2. 查询购买记录 - var result = await mediator.Send(query, cancellationToken); - - // 3. 返回结果 - return ApiResponse>.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StatisticsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StatisticsController.cs deleted file mode 100644 index cf4dd7f..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/StatisticsController.cs +++ /dev/null @@ -1,96 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Application.App.Statistics.Dto; -using TakeoutSaaS.Application.App.Statistics.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 统计数据接口。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/statistics")] -public sealed class StatisticsController(IMediator mediator) : BaseApiController -{ - /// - /// 获取订阅概览统计。 - /// - /// 取消标记。 - /// 订阅概览数据。 - [HttpGet("subscription-overview")] - [PermissionAuthorize("statistics:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> GetSubscriptionOverview(CancellationToken cancellationToken) - { - var result = await mediator.Send(new GetSubscriptionOverviewQuery(), cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 获取配额使用排行。 - /// - /// 配额类型。 - /// 返回前N条记录,默认10。 - /// 取消标记。 - /// 配额使用排行数据。 - [HttpGet("quota-ranking")] - [PermissionAuthorize("statistics:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> GetQuotaRanking( - [FromQuery] TenantQuotaType quotaType, - [FromQuery] int topN = 10, - CancellationToken cancellationToken = default) - { - var query = new GetQuotaUsageRankingQuery { QuotaType = quotaType, TopN = topN }; - var result = await mediator.Send(query, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 获取收入统计。 - /// - /// 统计月份数量,默认12个月。 - /// 取消标记。 - /// 收入统计数据。 - [HttpGet("revenue")] - [PermissionAuthorize("statistics:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> GetRevenue( - [FromQuery] int monthsCount = 12, - CancellationToken cancellationToken = default) - { - var query = new GetRevenueStatisticsQuery { MonthsCount = monthsCount }; - var result = await mediator.Send(query, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 获取即将到期的订阅列表。 - /// - /// 筛选天数,默认7天内到期。 - /// 是否只返回未开启自动续费的订阅。 - /// 取消标记。 - /// 即将到期的订阅列表。 - [HttpGet("expiring-subscriptions")] - [PermissionAuthorize("statistics:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetExpiringSubscriptions( - [FromQuery] int daysAhead = 7, - [FromQuery] bool onlyWithoutAutoRenew = false, - CancellationToken cancellationToken = default) - { - var query = new GetExpiringSubscriptionsQuery - { - DaysAhead = daysAhead, - OnlyWithoutAutoRenew = onlyWithoutAutoRenew - }; - var result = await mediator.Send(query, cancellationToken); - return ApiResponse>.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs deleted file mode 100644 index 710a8a5..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs +++ /dev/null @@ -1,216 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Application.App.Subscriptions.Queries; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 订阅管理。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/subscriptions")] -public sealed class SubscriptionsController(IMediator mediator) : BaseApiController -{ - /// - /// 分页查询订阅列表(支持按状态、套餐、到期时间筛选)。 - /// - /// 查询条件。 - /// 取消标记。 - /// 订阅分页结果。 - [HttpGet] - [PermissionAuthorize("subscription:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( - [FromQuery] GetSubscriptionListQuery query, - CancellationToken cancellationToken) - { - // 1. 查询订阅分页 - var result = await mediator.Send(query, cancellationToken); - - // 2. 返回结果 - return ApiResponse>.Ok(result); - } - - /// - /// 查看订阅详情(含套餐信息、配额使用、变更历史)。 - /// - /// 订阅 ID。 - /// 是否包含已软删除数据。 - /// 取消标记。 - /// 订阅详情或未找到。 - [HttpGet("{subscriptionId:long}")] - [PermissionAuthorize("subscription:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Detail( - long subscriptionId, - [FromQuery] bool includeDeleted, - CancellationToken cancellationToken) - { - // 1. 查询订阅详情 - var result = await mediator.Send(new GetSubscriptionDetailQuery - { - SubscriptionId = subscriptionId, - IncludeDeleted = includeDeleted - }, cancellationToken); - - // 2. 返回查询结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") - : ApiResponse.Ok(result); - } - - /// - /// 更新订阅基础信息(备注、自动续费等)。 - /// - /// 订阅 ID。 - /// 更新命令。 - /// 取消标记。 - /// 更新后的订阅详情或未找到。 - [HttpPut("{subscriptionId:long}")] - [PermissionAuthorize("subscription:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Update( - long subscriptionId, - [FromBody, Required] UpdateSubscriptionCommand command, - CancellationToken cancellationToken) - { - // 1. 绑定路由 ID - command = command with { SubscriptionId = subscriptionId }; - - // 2. 执行更新 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回更新结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") - : ApiResponse.Ok(result); - } - - /// - /// 延期订阅(增加订阅时长)。 - /// - /// 订阅 ID。 - /// 延期命令。 - /// 取消标记。 - /// 延期后的订阅详情或未找到。 - [HttpPost("{subscriptionId:long}/extend")] - [PermissionAuthorize("subscription:extend")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Extend( - long subscriptionId, - [FromBody, Required] ExtendSubscriptionCommand command, - CancellationToken cancellationToken) - { - // 1. 绑定路由 ID - command = command with { SubscriptionId = subscriptionId }; - - // 2. 执行延期 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回延期结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") - : ApiResponse.Ok(result); - } - - /// - /// 变更套餐(支持立即生效或下周期生效)。 - /// - /// 订阅 ID。 - /// 变更套餐命令。 - /// 取消标记。 - /// 变更后的订阅详情或未找到。 - [HttpPost("{subscriptionId:long}/change-plan")] - [PermissionAuthorize("subscription:change-plan")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> ChangePlan( - long subscriptionId, - [FromBody, Required] ChangeSubscriptionPlanCommand command, - CancellationToken cancellationToken) - { - // 1. 绑定路由 ID - command = command with { SubscriptionId = subscriptionId }; - - // 2. 执行套餐变更 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回变更结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") - : ApiResponse.Ok(result); - } - - /// - /// 变更订阅状态。 - /// - /// 订阅 ID。 - /// 状态变更命令。 - /// 取消标记。 - /// 变更后的订阅详情或未找到。 - [HttpPost("{subscriptionId:long}/status")] - [PermissionAuthorize("subscription:update-status")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> UpdateStatus( - long subscriptionId, - [FromBody, Required] UpdateSubscriptionStatusCommand command, - CancellationToken cancellationToken) - { - // 1. 绑定路由 ID - command = command with { SubscriptionId = subscriptionId }; - - // 2. 执行状态变更 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回变更结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") - : ApiResponse.Ok(result); - } - - /// - /// 批量延期订阅。 - /// - /// 批量延期命令。 - /// 取消标记。 - /// 批量延期结果。 - [HttpPost("batch-extend")] - [PermissionAuthorize("subscription:batch-extend")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> BatchExtend( - [FromBody, Required] BatchExtendSubscriptionsCommand command, - CancellationToken cancellationToken) - { - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 批量发送续费提醒。 - /// - /// 批量发送提醒命令。 - /// 取消标记。 - /// 批量发送提醒结果。 - [HttpPost("batch-remind")] - [PermissionAuthorize("subscription:batch-remind")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> BatchRemind( - [FromBody, Required] BatchSendReminderCommand command, - CancellationToken cancellationToken) - { - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs deleted file mode 100644 index 0bc4a42..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs +++ /dev/null @@ -1,332 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Swashbuckle.AspNetCore.Annotations; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 租户公告管理。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/announcements")] -public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiController -{ - /// - /// 分页查询公告。 - /// - /// - /// 示例: - /// - /// GET /api/admin/v1/tenants/100000000000000001/announcements?page=1&pageSize=20 - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "items": [], - /// "page": 1, - /// "pageSize": 20, - /// "totalCount": 0 - /// } - /// } - /// - /// - [HttpGet] - [PermissionAuthorize("tenant-announcement:read")] - [SwaggerOperation(Summary = "查询租户公告列表", Description = "需要权限:tenant-announcement:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task>> Search(long tenantId, [FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse>.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 绑定路由租户并查询列表 - var request = query with { TenantId = tenantId }; - - // 3. (空行后) 执行查询 - var result = await mediator.Send(request, cancellationToken); - - // 4. (空行后) 返回分页结果 - return ApiResponse>.Ok(result); - } - - /// - /// 公告详情。 - /// - /// - /// 示例: - /// - /// GET /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345 - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "tenantId": "100000000000000001", - /// "title": "租户公告", - /// "status": "Draft" - /// } - /// } - /// - /// - [HttpGet("{announcementId:long}")] - [PermissionAuthorize("tenant-announcement:read")] - [SwaggerOperation(Summary = "获取公告详情", Description = "需要权限:tenant-announcement:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Detail(long tenantId, long announcementId, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 查询公告详情 - var query = new GetAnnouncementByIdQuery { TenantId = tenantId, AnnouncementId = announcementId }; - var result = await mediator.Send(query, cancellationToken); - - // 3. (空行后) 返回详情或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } - - /// - /// 创建公告。 - /// - /// - /// 示例: - /// - /// POST /api/admin/v1/tenants/100000000000000001/announcements - /// Body: - /// { - /// "title": "租户公告", - /// "content": "新品上线提醒", - /// "announcementType": 0, - /// "priority": 5, - /// "effectiveFrom": "2025-12-20T00:00:00Z", - /// "targetType": "roles", - /// "targetParameters": "{\"roles\":[\"OpsManager\"]}" - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "tenantId": "100000000000000001", - /// "title": "租户公告", - /// "status": "Draft" - /// } - /// } - /// - /// - [HttpPost] - [PermissionAuthorize("tenant-announcement:create")] - [SwaggerOperation(Summary = "创建租户公告", Description = "需要权限:tenant-announcement:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 绑定租户标识并创建公告 - command = command with { TenantId = tenantId }; - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 更新公告(仅草稿)。 - /// - /// - /// 示例: - /// - /// PUT /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345 - /// Body: - /// { - /// "title": "租户公告(更新)", - /// "content": "公告内容更新", - /// "targetType": "all", - /// "targetParameters": null, - /// "rowVersion": "AAAAAAAAB9E=" - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "status": "Draft" - /// } - /// } - /// - /// - [HttpPut("{announcementId:long}")] - [PermissionAuthorize("tenant-announcement:update")] - [SwaggerOperation(Summary = "更新租户公告", Description = "仅草稿可更新;需要权限:tenant-announcement:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 执行更新 - command = command with { TenantId = tenantId, AnnouncementId = announcementId }; - var result = await mediator.Send(command, cancellationToken); - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } - - /// - /// 发布公告。 - /// - /// - /// 示例: - /// - /// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/publish - /// Body: - /// { - /// "rowVersion": "AAAAAAAAB9E=" - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "status": "Published" - /// } - /// } - /// - /// - [HttpPost("{announcementId:long}/publish")] - [PermissionAuthorize("tenant-announcement:publish")] - [SwaggerOperation(Summary = "发布租户公告", Description = "需要权限:tenant-announcement:publish")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Publish(long tenantId, long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 发布公告 - command = command with { TenantId = tenantId, AnnouncementId = announcementId }; - var result = await mediator.Send(command, cancellationToken); - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } - - /// - /// 撤销公告。 - /// - /// - /// 示例: - /// - /// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/revoke - /// Body: - /// { - /// "rowVersion": "AAAAAAAAB9E=" - /// } - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "status": "Revoked" - /// } - /// } - /// - /// - [HttpPost("{announcementId:long}/revoke")] - [PermissionAuthorize("tenant-announcement:revoke")] - [SwaggerOperation(Summary = "撤销租户公告", Description = "需要权限:tenant-announcement:revoke")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Revoke(long tenantId, long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 撤销公告 - command = command with { TenantId = tenantId, AnnouncementId = announcementId }; - var result = await mediator.Send(command, cancellationToken); - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } - - /// - /// 删除公告。 - /// - /// - /// 示例: - /// - /// DELETE /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345 - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": true - /// } - /// - /// - [HttpDelete("{announcementId:long}")] - [PermissionAuthorize("tenant-announcement:delete")] - [SwaggerOperation(Summary = "删除租户公告", Description = "需要权限:tenant-announcement:delete")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> Delete(long tenantId, long announcementId, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 执行删除 - var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken); - return ApiResponse.Ok(result); - } - -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantPackagesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantPackagesController.cs deleted file mode 100644 index a7327d2..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantPackagesController.cs +++ /dev/null @@ -1,177 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 租户套餐管理。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/tenant-packages")] -public sealed class TenantPackagesController(IMediator mediator) : BaseApiController -{ - /// - /// 分页查询租户套餐。 - /// - /// 查询条件。 - /// 取消标记。 - /// 租户套餐分页结果。 - [HttpGet] - [PermissionAuthorize("tenant-package:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Search([FromQuery] SearchTenantPackagesQuery query, CancellationToken cancellationToken) - { - // 1. 查询租户套餐分页 - var result = await mediator.Send(query, cancellationToken); - - // 2. 返回结果 - return ApiResponse>.Ok(result); - } - - /// - /// 查询套餐使用统计(订阅关联数量、使用租户数量)。 - /// - /// 套餐 ID 列表(为空表示查询全部)。 - /// 取消标记。 - /// 套餐使用统计列表。 - [HttpGet("usages")] - [PermissionAuthorize("tenant-package:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Usages( - [FromQuery] long[]? tenantPackageIds, - CancellationToken cancellationToken) - { - // 1. 查询使用统计 - var result = await mediator.Send(new GetTenantPackageUsagesQuery { TenantPackageIds = tenantPackageIds }, cancellationToken); - - // 2. 返回结果 - return ApiResponse>.Ok(result); - } - - /// - /// 查询套餐当前使用租户列表(按有效订阅口径)。 - /// - /// 套餐 ID。 - /// 关键词(可选)。 - /// 可选:未来 N 天内到期筛选。 - /// 页码(从 1 开始)。 - /// 每页大小。 - /// 取消标记。 - /// 使用租户分页结果。 - [HttpGet("{tenantPackageId:long}/tenants")] - [PermissionAuthorize("tenant-package:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Tenants( - long tenantPackageId, - [FromQuery] string? keyword, - [FromQuery] int? expiringWithinDays, - [FromQuery] int page = 1, - [FromQuery] int pageSize = 20, - CancellationToken cancellationToken = default) - { - // 1. 查询套餐使用租户分页 - var result = await mediator.Send(new GetTenantPackageTenantsQuery - { - TenantPackageId = tenantPackageId, - Keyword = keyword, - ExpiringWithinDays = expiringWithinDays, - Page = page, - PageSize = pageSize - }, cancellationToken); - - // 2. 返回结果 - return ApiResponse>.Ok(result); - } - - /// - /// 查看套餐详情。 - /// - /// 套餐 ID。 - /// 取消标记。 - /// 套餐详情或未找到。 - [HttpGet("{tenantPackageId:long}")] - [PermissionAuthorize("tenant-package:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Detail(long tenantPackageId, CancellationToken cancellationToken) - { - // 1. 查询套餐详情 - var result = await mediator.Send(new GetTenantPackageByIdQuery { TenantPackageId = tenantPackageId }, cancellationToken); - - // 2. 返回查询结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "套餐不存在") - : ApiResponse.Ok(result); - } - - /// - /// 创建套餐。 - /// - /// 创建命令。 - /// 取消标记。 - /// 创建后的套餐。 - [HttpPost] - [PermissionAuthorize("tenant-package:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Create([FromBody, Required] CreateTenantPackageCommand command, CancellationToken cancellationToken) - { - // 1. 执行创建 - var result = await mediator.Send(command, cancellationToken); - - // 2. 返回创建结果 - return ApiResponse.Ok(result); - } - - /// - /// 更新套餐。 - /// - /// 套餐 ID。 - /// 更新命令。 - /// 取消标记。 - /// 更新后的套餐或未找到。 - [HttpPut("{tenantPackageId:long}")] - [PermissionAuthorize("tenant-package:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Update(long tenantPackageId, [FromBody, Required] UpdateTenantPackageCommand command, CancellationToken cancellationToken) - { - // 1. 绑定路由 ID - command = command with { TenantPackageId = tenantPackageId }; - - // 2. 执行更新 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回更新结果或 404 - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "套餐不存在") - : ApiResponse.Ok(result); - } - - /// - /// 删除套餐。 - /// - /// 套餐 ID。 - /// 取消标记。 - /// 删除结果。 - [HttpDelete("{tenantPackageId:long}")] - [PermissionAuthorize("tenant-package:delete")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Delete(long tenantPackageId, CancellationToken cancellationToken) - { - // 1. 构建删除命令 - var command = new DeleteTenantPackageCommand { TenantPackageId = tenantPackageId }; - - // 2. 执行删除并返回 - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs deleted file mode 100644 index 9beaf81..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs +++ /dev/null @@ -1,395 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 租户管理。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/tenants")] -public sealed class TenantsController(IMediator mediator) : BaseApiController -{ - /// - /// 注册租户并初始化套餐。 - /// - /// 注册的租户信息。 - [HttpPost] - [PermissionAuthorize("tenant:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Register([FromBody] RegisterTenantCommand command, CancellationToken cancellationToken) - { - // 1. 注册租户并初始化套餐 - var result = await mediator.Send(command, cancellationToken); - - // 2. 返回注册结果 - return ApiResponse.Ok(result); - } - - /// - /// 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号)。 - /// - /// 新增后的租户详情。 - [HttpPost("manual")] - [PermissionAuthorize("tenant:create")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> CreateManually([FromBody] CreateTenantManuallyCommand command, CancellationToken cancellationToken) - { - // 1. 后台手动新增租户(直接可用) - var result = await mediator.Send(command, cancellationToken); - - // 2. 返回创建结果 - return ApiResponse.Ok(result); - } - - /// - /// 分页查询租户。 - /// - /// 租户分页结果。 - [HttpGet] - [PermissionAuthorize("tenant:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Search([FromQuery] SearchTenantsQuery query, CancellationToken cancellationToken) - { - // 1. 查询租户分页 - var result = await mediator.Send(query, cancellationToken); - - // 2. 返回分页数据 - return ApiResponse>.Ok(result); - } - - /// - /// 查看租户详情。 - /// - /// 租户详情。 - [HttpGet("{tenantId:long}")] - [PermissionAuthorize("tenant:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Detail(long tenantId, CancellationToken cancellationToken) - { - // 1. 查询租户详情 - var result = await mediator.Send(new GetTenantByIdQuery(tenantId), cancellationToken); - - // 2. 返回租户信息 - return ApiResponse.Ok(result); - } - - /// - /// 更新租户基础信息。 - /// - /// 租户 ID。 - /// 更新命令。 - /// 取消标记。 - /// 更新结果。 - [HttpPut("{tenantId:long}")] - [PermissionAuthorize("tenant:update")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Update( - long tenantId, - [FromBody, Required] UpdateTenantCommand body, - CancellationToken cancellationToken) - { - // 1. 校验路由与请求体租户标识一致 - if (body.TenantId != 0 && body.TenantId != tenantId) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "路由 tenantId 与请求体 tenantId 不一致"); - } - - // 2. 绑定租户标识并执行更新(若不存在或冲突则抛出业务异常,由全局异常处理转换为 404/409) - var command = body with { TenantId = tenantId }; - await mediator.Send(command, cancellationToken); - - // 3. 返回成功结果 - return ApiResponse.Ok(null); - } - - /// - /// 提交或更新实名认证资料。 - /// - /// 提交的实名认证信息。 - [HttpPost("{tenantId:long}/verification")] - [PermissionAuthorize("tenant:review")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> SubmitVerification( - long tenantId, - [FromBody] SubmitTenantVerificationCommand body, - CancellationToken cancellationToken) - { - // 1. 合并路由中的租户标识 - var command = body with { TenantId = tenantId }; - - // 2. 提交或更新认证资料 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回认证结果 - return ApiResponse.Ok(result); - } - - /// - /// 审核租户。 - /// - /// 审核后的租户信息。 - [HttpPost("{tenantId:long}/review")] - [PermissionAuthorize("tenant:review")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Review(long tenantId, [FromBody] ReviewTenantCommand body, CancellationToken cancellationToken) - { - // 1. 绑定租户标识 - var command = body with { TenantId = tenantId }; - - // 2. 执行审核 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回审核结果 - return ApiResponse.Ok(result); - } - - /// - /// 查询当前租户审核领取信息。 - /// - /// 领取信息,未领取返回 null。 - [HttpGet("{tenantId:long}/review/claim")] - [PermissionAuthorize("tenant:review")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> GetReviewClaim(long tenantId, CancellationToken cancellationToken) - { - // 1. 查询领取信息 - var result = await mediator.Send(new GetTenantReviewClaimQuery(tenantId), cancellationToken); - - // 2. 返回领取信息 - return ApiResponse.Ok(result); - } - - /// - /// 领取租户入驻审核(领取后仅领取人可操作审核)。 - /// - /// 领取结果。 - [HttpPost("{tenantId:long}/review/claim")] - [PermissionAuthorize("tenant:review")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> ClaimReview(long tenantId, CancellationToken cancellationToken) - { - // 1. 执行领取 - var result = await mediator.Send(new ClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken); - - // 2. 返回领取结果 - return ApiResponse.Ok(result); - } - - /// - /// 强制接管租户入驻审核(仅超级管理员可用)。 - /// - /// 接管后的领取信息。 - [HttpPost("{tenantId:long}/review/force-claim")] - [PermissionAuthorize("tenant:review:force-claim")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> ForceClaimReview(long tenantId, CancellationToken cancellationToken) - { - // 1. 执行强制接管 - var result = await mediator.Send(new ForceClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken); - - // 2. 返回接管结果 - return ApiResponse.Ok(result); - } - - /// - /// 释放租户入驻审核领取(仅领取人可释放)。 - /// - /// 释放后的领取信息,未领取返回 null。 - [HttpPost("{tenantId:long}/review/release")] - [PermissionAuthorize("tenant:review")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> ReleaseReview(long tenantId, CancellationToken cancellationToken) - { - // 1. 执行释放 - var result = await mediator.Send(new ReleaseTenantReviewClaimCommand { TenantId = tenantId }, cancellationToken); - - // 2. 返回释放结果 - return ApiResponse.Ok(result); - } - - /// - /// 冻结租户(暂停服务)。 - /// - /// 冻结后的租户信息。 - [HttpPost("{tenantId:long}/freeze")] - [PermissionAuthorize("tenant:review")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Freeze( - long tenantId, - [FromBody] FreezeTenantCommand body, - CancellationToken cancellationToken) - { - // 1. 合并路由参数 - var command = body with { TenantId = tenantId }; - - // 2. 执行冻结 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回冻结结果 - return ApiResponse.Ok(result); - } - - /// - /// 解冻租户(恢复服务)。 - /// - /// 解冻后的租户信息。 - [HttpPost("{tenantId:long}/unfreeze")] - [PermissionAuthorize("tenant:review")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Unfreeze( - long tenantId, - [FromBody] UnfreezeTenantCommand body, - CancellationToken cancellationToken) - { - // 1. 合并路由参数 - var command = body with { TenantId = tenantId }; - - // 2. 执行解冻 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回解冻结果 - return ApiResponse.Ok(result); - } - - /// - /// 创建或续费租户订阅。 - /// - /// 创建或续费的订阅信息。 - [HttpPost("{tenantId:long}/subscriptions")] - [PermissionAuthorize("tenant:subscription")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> CreateSubscription( - long tenantId, - [FromBody] CreateTenantSubscriptionCommand body, - CancellationToken cancellationToken) - { - // 1. 绑定租户并创建或续费订阅 - var command = body with { TenantId = tenantId }; - - // 2. 返回订阅结果 - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 延期/赠送租户订阅时长(按当前订阅套餐续费)。 - /// - /// 续费后的订阅信息。 - [HttpPost("{tenantId:long}/subscriptions/extend")] - [PermissionAuthorize("tenant:subscription")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> ExtendSubscription( - long tenantId, - [FromBody] ExtendTenantSubscriptionCommand body, - CancellationToken cancellationToken) - { - // 1. 合并租户标识 - var command = body with { TenantId = tenantId }; - - // 2. 执行延期/赠送 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回订阅结果 - return ApiResponse.Ok(result); - } - - /// - /// 套餐升降配。 - /// - /// 更新后的订阅信息。 - [HttpPut("{tenantId:long}/subscriptions/{subscriptionId:long}/plan")] - [PermissionAuthorize("tenant:subscription")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> ChangePlan( - long tenantId, - long subscriptionId, - [FromBody] ChangeTenantSubscriptionPlanCommand body, - CancellationToken cancellationToken) - { - // 1. 绑定租户与订阅标识 - var command = body with { TenantId = tenantId, TenantSubscriptionId = subscriptionId }; - - // 2. 执行升降配 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回调整后的订阅 - return ApiResponse.Ok(result); - } - - /// - /// 查询审核日志。 - /// - /// 租户审核日志分页结果。 - [HttpGet("{tenantId:long}/audits")] - [PermissionAuthorize("tenant:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> AuditLogs( - long tenantId, - [FromQuery] int page = 1, - [FromQuery] int pageSize = 20, - CancellationToken cancellationToken = default) - { - // 1. 构造审核日志查询 - var query = new GetTenantAuditLogsQuery(tenantId, page, pageSize); - - // 2. 查询并返回分页结果 - var result = await mediator.Send(query, cancellationToken); - return ApiResponse>.Ok(result); - } - - /// - /// 配额校验并占用额度(门店/账号/短信/配送)。 - /// - /// 租户标识来自路由参数 tenantId,无需强制使用租户请求头。 - /// 配额校验结果。 - [HttpPost("{tenantId:long}/quotas/check")] - [PermissionAuthorize("tenant:quota:check")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> CheckQuota( - long tenantId, - [FromBody, Required] CheckTenantQuotaCommand body, - CancellationToken cancellationToken) - { - // 1. 绑定租户标识 - var command = body with { TenantId = tenantId }; - - // 2. 校验并占用配额 - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 分页查询租户配额使用历史。 - /// - /// 租户 ID。 - /// 查询条件。 - /// 取消标记。 - /// 配额使用历史分页结果。 - [HttpGet("{tenantId:long}/quota-usage-history")] - [PermissionAuthorize("tenant:quota:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetQuotaUsageHistory( - long tenantId, - [FromQuery] GetTenantQuotaUsageHistoryQuery query, - CancellationToken cancellationToken) - { - // 1. 绑定租户标识 - query = query with { TenantId = tenantId }; - - // 2. 查询配额使用历史 - var result = await mediator.Send(query, cancellationToken); - - // 3. 返回分页结果 - return ApiResponse>.Ok(result); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs b/src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs deleted file mode 100644 index ccea256..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Text.Json; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; - -namespace TakeoutSaaS.Application.App.Billings; - -/// -/// 账单 DTO 映射助手。 -/// -internal static class BillingMapping -{ - /// - /// 将账单实体映射为账单 DTO(旧版)。 - /// - /// 账单实体。 - /// 租户名称。 - /// 账单 DTO。 - public static BillDto ToDto(this TenantBillingStatement bill, string? tenantName = null) - => new() - { - Id = bill.Id, - TenantId = bill.TenantId, - TenantName = tenantName, - StatementNo = bill.StatementNo, - PeriodStart = bill.PeriodStart, - PeriodEnd = bill.PeriodEnd, - AmountDue = bill.AmountDue, - AmountPaid = bill.AmountPaid, - Status = bill.Status, - DueDate = bill.DueDate, - CreatedAt = bill.CreatedAt - }; - - /// - /// 将账单实体映射为账单列表 DTO(新版)。 - /// - /// 账单实体。 - /// 租户名称。 - /// 账单列表 DTO。 - public static BillingListDto ToBillingListDto(this TenantBillingStatement billing, string? tenantName = null) - => new() - { - Id = billing.Id, - TenantId = billing.TenantId, - SubscriptionId = billing.SubscriptionId, - TenantName = tenantName ?? string.Empty, - StatementNo = billing.StatementNo, - BillingType = billing.BillingType, - Status = billing.Status, - PeriodStart = billing.PeriodStart, - PeriodEnd = billing.PeriodEnd, - AmountDue = billing.AmountDue, - AmountPaid = billing.AmountPaid, - DiscountAmount = billing.DiscountAmount, - TaxAmount = billing.TaxAmount, - TotalAmount = billing.CalculateTotalAmount(), - Currency = billing.Currency, - DueDate = billing.DueDate, - CreatedAt = billing.CreatedAt, - UpdatedAt = billing.UpdatedAt, - IsOverdue = billing.Status == TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Overdue - || (billing.Status == TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Pending && billing.DueDate < DateTime.UtcNow), - OverdueDays = (billing.Status is TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Pending - or TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Overdue) - && billing.DueDate < DateTime.UtcNow - ? (int)(DateTime.UtcNow - billing.DueDate).TotalDays - : 0 - }; - - /// - /// 将账单实体与支付记录映射为账单详情 DTO(旧版)。 - /// - /// 账单实体。 - /// 支付记录列表。 - /// 租户名称。 - /// 账单详情 DTO。 - public static BillDetailDto ToDetailDto( - this TenantBillingStatement bill, - List payments, - string? tenantName = null) - => new() - { - Id = bill.Id, - TenantId = bill.TenantId, - TenantName = tenantName, - StatementNo = bill.StatementNo, - PeriodStart = bill.PeriodStart, - PeriodEnd = bill.PeriodEnd, - AmountDue = bill.AmountDue, - AmountPaid = bill.AmountPaid, - Status = bill.Status, - DueDate = bill.DueDate, - LineItemsJson = bill.LineItemsJson, - CreatedAt = bill.CreatedAt, - Payments = payments.Select(p => p.ToDto()).ToList() - }; - - /// - /// 将账单实体与支付记录映射为账单详情 DTO(新版)。 - /// - /// 账单实体。 - /// 支付记录列表。 - /// 租户名称。 - /// 账单详情 DTO。 - public static BillingDetailDto ToBillingDetailDto( - this TenantBillingStatement billing, - List payments, - string? tenantName = null) - { - // 反序列化账单明细 - var lineItems = new List(); - if (!string.IsNullOrWhiteSpace(billing.LineItemsJson)) - { - try - { - lineItems = JsonSerializer.Deserialize>(billing.LineItemsJson) ?? []; - } - catch - { - lineItems = []; - } - } - - return new BillingDetailDto - { - Id = billing.Id, - TenantId = billing.TenantId, - TenantName = tenantName ?? string.Empty, - SubscriptionId = billing.SubscriptionId, - StatementNo = billing.StatementNo, - BillingType = billing.BillingType, - Status = billing.Status, - PeriodStart = billing.PeriodStart, - PeriodEnd = billing.PeriodEnd, - AmountDue = billing.AmountDue, - AmountPaid = billing.AmountPaid, - DiscountAmount = billing.DiscountAmount, - TaxAmount = billing.TaxAmount, - TotalAmount = billing.CalculateTotalAmount(), - Currency = billing.Currency, - DueDate = billing.DueDate, - ReminderSentAt = billing.ReminderSentAt, - OverdueNotifiedAt = billing.OverdueNotifiedAt, - LineItemsJson = billing.LineItemsJson, - LineItems = lineItems, - Payments = payments.Select(p => p.ToPaymentRecordDto()).ToList(), - Notes = billing.Notes, - CreatedAt = billing.CreatedAt, - CreatedBy = billing.CreatedBy, - UpdatedAt = billing.UpdatedAt, - UpdatedBy = billing.UpdatedBy - }; - } - - /// - /// 将支付记录实体映射为支付 DTO(旧版)。 - /// - /// 支付记录实体。 - /// 支付 DTO。 - public static PaymentDto ToDto(this TenantPayment payment) - => new() - { - Id = payment.Id, - BillingStatementId = payment.BillingStatementId, - Amount = payment.Amount, - Method = payment.Method, - Status = payment.Status, - TransactionNo = payment.TransactionNo, - ProofUrl = payment.ProofUrl, - PaidAt = payment.PaidAt, - Notes = payment.Notes, - CreatedAt = payment.CreatedAt - }; - - /// - /// 将支付记录实体映射为支付记录 DTO(新版)。 - /// - /// 支付记录实体。 - /// 支付记录 DTO。 - public static PaymentRecordDto ToPaymentRecordDto(this TenantPayment payment) - => new() - { - Id = payment.Id, - TenantId = payment.TenantId, - BillingId = payment.BillingStatementId, - Amount = payment.Amount, - Method = payment.Method, - Status = payment.Status, - TransactionNo = payment.TransactionNo, - ProofUrl = payment.ProofUrl, - IsVerified = payment.VerifiedAt.HasValue, - PaidAt = payment.PaidAt, - VerifiedBy = payment.VerifiedBy, - VerifiedAt = payment.VerifiedAt, - RefundReason = payment.RefundReason, - RefundedAt = payment.RefundedAt, - Notes = payment.Notes, - CreatedAt = payment.CreatedAt - }; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/BatchUpdateStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/BatchUpdateStatusCommand.cs deleted file mode 100644 index 054a9e4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/BatchUpdateStatusCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using MediatR; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 批量更新账单状态命令。 -/// -public sealed record BatchUpdateStatusCommand : IRequest -{ - /// - /// 账单 ID 列表(雪花算法)。 - /// - public long[] BillingIds { get; init; } = []; - - /// - /// 新状态。 - /// - public TenantBillingStatus NewStatus { get; init; } - - /// - /// 批量操作备注。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CancelBillingCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CancelBillingCommand.cs deleted file mode 100644 index ed38f36..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CancelBillingCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 取消账单命令。 -/// -public sealed record CancelBillingCommand : IRequest -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillingId { get; init; } - - /// - /// 取消原因。 - /// - public string Reason { get; init; } = string.Empty; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/ConfirmPaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/ConfirmPaymentCommand.cs deleted file mode 100644 index db00c49..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/ConfirmPaymentCommand.cs +++ /dev/null @@ -1,42 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 一键确认收款命令(记录支付 + 立即审核通过)。 -/// -public sealed record ConfirmPaymentCommand : IRequest -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillingId { get; init; } - - /// - /// 支付金额。 - /// - public decimal Amount { get; init; } - - /// - /// 支付方式。 - /// - public TenantPaymentMethod Method { get; init; } - - /// - /// 交易号。 - /// - public string? TransactionNo { get; init; } - - /// - /// 支付凭证 URL。 - /// - public string? ProofUrl { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillCommand.cs deleted file mode 100644 index d22ecde..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 创建账单命令。 -/// -public sealed record CreateBillCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillingCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillingCommand.cs deleted file mode 100644 index d4abe30..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillingCommand.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 创建账单命令。 -/// -public sealed record CreateBillingCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 账单类型。 - /// - public BillingType BillingType { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 账单明细列表。 - /// - public List LineItems { get; init; } = []; - - /// - /// 备注。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/GenerateSubscriptionBillingCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/GenerateSubscriptionBillingCommand.cs deleted file mode 100644 index be76d6b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/GenerateSubscriptionBillingCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 生成订阅账单命令(自动化场景)。 -/// -public sealed record GenerateSubscriptionBillingCommand : IRequest -{ - /// - /// 订阅 ID(雪花算法)。 - /// - public long SubscriptionId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/ProcessOverdueBillingsCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/ProcessOverdueBillingsCommand.cs deleted file mode 100644 index 11d9d23..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/ProcessOverdueBillingsCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 处理逾期账单命令(后台任务场景)。 -/// -public sealed record ProcessOverdueBillingsCommand : IRequest -{ -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/RecordPaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/RecordPaymentCommand.cs deleted file mode 100644 index 46f94e0..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/RecordPaymentCommand.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 记录支付命令。 -/// -public sealed record RecordPaymentCommand : IRequest -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillingId { get; init; } - - /// - /// 支付金额。 - /// - public decimal Amount { get; init; } - - /// - /// 支付方式。 - /// - public TenantPaymentMethod Method { get; init; } - - /// - /// 交易号。 - /// - public string? TransactionNo { get; init; } - - /// - /// 支付凭证 URL。 - /// - public string? ProofUrl { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillStatusCommand.cs deleted file mode 100644 index fcbff76..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillStatusCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 更新账单状态命令。 -/// -public sealed record UpdateBillStatusCommand : IRequest -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillId { get; init; } - - /// - /// 新状态。 - /// - public TenantBillingStatus Status { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillingStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillingStatusCommand.cs deleted file mode 100644 index d6abcff..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillingStatusCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using MediatR; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 更新账单状态命令。 -/// -public sealed record UpdateBillingStatusCommand : IRequest -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillingId { get; init; } - - /// - /// 新状态。 - /// - public TenantBillingStatus NewStatus { get; init; } - - /// - /// 备注。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/VerifyPaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/VerifyPaymentCommand.cs deleted file mode 100644 index b652acd..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/VerifyPaymentCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Commands; - -/// -/// 审核支付命令。 -/// -public sealed record VerifyPaymentCommand : IRequest -{ - /// - /// 支付记录 ID(雪花算法)。 - /// - [Required] - public long PaymentId { get; init; } - - /// - /// 是否通过审核。 - /// - public bool Approved { get; init; } - - /// - /// 审核备注(可选)。 - /// - [MaxLength(512)] - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDetailDto.cs deleted file mode 100644 index 36b6851..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDetailDto.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单详情 DTO。 -/// -public sealed record BillDetailDto -{ - /// - /// 账单 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 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; } - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 账单明细 JSON。 - /// - public string? LineItemsJson { get; init; } - - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 支付记录列表。 - /// - public List Payments { get; init; } = new(); -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDto.cs deleted file mode 100644 index dfc76d1..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDto.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单 DTO。 -/// -public sealed record BillDto -{ - /// - /// 账单 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 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; } - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs deleted file mode 100644 index 02426cd..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDetailDto.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单详情 DTO(管理员端)。 -/// -public sealed record BillingDetailDto -{ - /// - /// 账单 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 账单编号。 - /// - public string StatementNo { get; init; } = string.Empty; - - /// - /// 计费周期开始时间(UTC)。 - /// - public DateTime PeriodStart { get; init; } - - /// - /// 计费周期结束时间(UTC)。 - /// - public DateTime PeriodEnd { get; init; } - - /// - /// 账单类型。 - /// - public BillingType BillingType { get; init; } - - /// - /// 账单状态。 - /// - public TenantBillingStatus Status { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 已支付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 折扣金额。 - /// - public decimal DiscountAmount { get; init; } - - /// - /// 税费金额。 - /// - public decimal TaxAmount { get; init; } - - /// - /// 总金额(应付金额 - 折扣 + 税费)。 - /// - public decimal TotalAmount { get; init; } - - /// - /// 币种。 - /// - public string Currency { get; init; } = "CNY"; - - /// - /// 到期日。 - /// - public DateTime DueDate { get; init; } - - /// - /// 订阅 ID(可选)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? SubscriptionId { get; init; } - - /// - /// 账单明细 JSON(原始字符串)。 - /// - public string? LineItemsJson { get; init; } - - /// - /// 账单明细行项目。 - /// - public IReadOnlyList LineItems { get; init; } = []; - - /// - /// 支付记录。 - /// - public IReadOnlyList Payments { get; init; } = []; - - /// - /// 提醒发送时间。 - /// - public DateTime? ReminderSentAt { get; init; } - - /// - /// 逾期通知时间。 - /// - public DateTime? OverdueNotifiedAt { get; init; } - - /// - /// 备注。 - /// - public string? Notes { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 创建人 ID。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? CreatedBy { get; init; } - - /// - /// 更新时间。 - /// - public DateTime? UpdatedAt { get; init; } - - /// - /// 更新人 ID。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? UpdatedBy { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs deleted file mode 100644 index b14feab..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingDtos.cs +++ /dev/null @@ -1,545 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto.Legacy; - -/// -/// 账单列表 DTO(用于列表展示)。 -/// -public sealed record BillingListDto -{ - /// - /// 账单 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 关联订阅 ID(仅订阅/续费账单可能有值)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? SubscriptionId { get; init; } - - /// - /// 账单编号。 - /// - public string StatementNo { get; init; } = string.Empty; - - /// - /// 账单类型。 - /// - public BillingType BillingType { get; init; } - - /// - /// 计费周期开始时间(UTC)。 - /// - public DateTime PeriodStart { get; init; } - - /// - /// 计费周期结束时间(UTC)。 - /// - public DateTime PeriodEnd { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 折扣金额。 - /// - public decimal DiscountAmount { get; init; } - - /// - /// 税费金额。 - /// - public decimal TaxAmount { get; init; } - - /// - /// 总金额(应付金额 - 折扣 + 税费)。 - /// - public decimal TotalAmount { get; init; } - - /// - /// 已付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 币种。 - /// - public string Currency { get; init; } = "CNY"; - - /// - /// 账单状态。 - /// - public TenantBillingStatus Status { get; init; } - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 是否已逾期(根据到期日与状态综合判断)。 - /// - public bool IsOverdue { get; init; } - - /// - /// 逾期天数(未逾期为 0)。 - /// - public int OverdueDays { get; init; } - - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 更新时间(UTC)。 - /// - public DateTime? UpdatedAt { get; init; } -} - -/// -/// 账单详情 DTO(含明细项)。 -/// -public sealed record BillingDetailDto -{ - /// - /// 账单 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 账单编号。 - /// - public string StatementNo { get; init; } = string.Empty; - - /// - /// 关联订阅 ID(仅订阅/续费账单可能有值)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? SubscriptionId { get; init; } - - /// - /// 账单类型。 - /// - public BillingType BillingType { get; init; } - - /// - /// 计费周期开始时间(UTC)。 - /// - public DateTime PeriodStart { get; init; } - - /// - /// 计费周期结束时间(UTC)。 - /// - public DateTime PeriodEnd { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 折扣金额。 - /// - public decimal DiscountAmount { get; init; } - - /// - /// 税费金额。 - /// - public decimal TaxAmount { get; init; } - - /// - /// 总金额(应付金额 - 折扣 + 税费)。 - /// - public decimal TotalAmount { get; init; } - - /// - /// 已付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 币种。 - /// - public string Currency { get; init; } = "CNY"; - - /// - /// 账单状态。 - /// - public TenantBillingStatus Status { get; init; } - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 账单明细 JSON。 - /// - public string? LineItemsJson { get; init; } - - /// - /// 账单明细列表(从 JSON 反序列化)。 - /// - public IReadOnlyList LineItems { get; init; } = []; - - /// - /// 支付记录列表。 - /// - public IReadOnlyList Payments { get; init; } = []; - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } - - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 更新时间(UTC)。 - /// - public DateTime? UpdatedAt { get; init; } -} - -/// -/// 账单明细项 DTO。 -/// -public sealed record BillingLineItemDto -{ - /// - /// 明细类型(如:套餐费、配额包费用、其他费用)。 - /// - public string ItemType { get; init; } = string.Empty; - - /// - /// 描述。 - /// - public string Description { get; init; } = string.Empty; - - /// - /// 数量。 - /// - public decimal Quantity { get; init; } - - /// - /// 单价。 - /// - public decimal UnitPrice { get; init; } - - /// - /// 金额(数量 × 单价)。 - /// - public decimal Amount { get; init; } - - /// - /// 折扣率(0-1)。 - /// - public decimal? DiscountRate { get; init; } -} - -/// -/// 支付记录 DTO。 -/// -public sealed record PaymentRecordDto -{ - /// - /// 支付记录 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 账单 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long BillingId { get; init; } - - /// - /// 支付金额。 - /// - public decimal Amount { get; init; } - - /// - /// 支付方式。 - /// - public TenantPaymentMethod Method { get; init; } - - /// - /// 支付状态。 - /// - public TenantPaymentStatus Status { get; init; } - - /// - /// 支付流水号。 - /// - public string? TransactionNo { get; init; } - - /// - /// 支付凭证 URL。 - /// - public string? ProofUrl { get; init; } - - /// - /// 备注。 - /// - public string? Notes { get; init; } - - /// - /// 审核状态(待审核/已通过/已拒绝)。 - /// - public bool IsVerified { get; init; } - - /// - /// 审核人 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? VerifiedBy { get; init; } - - /// - /// 审核时间(UTC)。 - /// - public DateTime? VerifiedAt { get; init; } - - /// - /// 退款原因。 - /// - public string? RefundReason { get; init; } - - /// - /// 退款时间(UTC)。 - /// - public DateTime? RefundedAt { get; init; } - - /// - /// 支付时间(UTC)。 - /// - public DateTime? PaidAt { get; init; } - - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; init; } -} - -/// -/// 账单统计 DTO。 -/// -public sealed record BillingStatisticsDto -{ - /// - /// 租户 ID(为空表示跨租户统计)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? TenantId { get; init; } - - /// - /// 统计周期开始时间(UTC)。 - /// - public DateTime StartDate { get; init; } - - /// - /// 统计周期结束时间(UTC)。 - /// - public DateTime EndDate { get; init; } - - /// - /// 分组方式(Day/Week/Month)。 - /// - public string GroupBy { get; init; } = "Day"; - - /// - /// 总账单数量。 - /// - public int TotalCount { get; init; } - - /// - /// 待付款账单数量。 - /// - public int PendingCount { get; init; } - - /// - /// 已付款账单数量。 - /// - public int PaidCount { get; init; } - - /// - /// 逾期账单数量。 - /// - public int OverdueCount { get; init; } - - /// - /// 已取消账单数量。 - /// - public int CancelledCount { get; init; } - - /// - /// 总应收金额(账单原始应付)。 - /// - public decimal TotalAmountDue { get; init; } - - /// - /// 总实收金额。 - /// - public decimal TotalAmountPaid { get; init; } - - /// - /// 总未收金额(总金额 - 实收)。 - /// - public decimal TotalAmountUnpaid { get; init; } - - /// - /// 逾期未收金额。 - /// - public decimal TotalOverdueAmount { get; init; } - - /// - /// 分组统计:应收金额趋势(Key 为分组起始日期 yyyy-MM-dd)。 - /// - public Dictionary AmountDueTrend { get; init; } = []; - - /// - /// 分组统计:实收金额趋势(Key 为分组起始日期 yyyy-MM-dd)。 - /// - public Dictionary AmountPaidTrend { get; init; } = []; - - /// - /// 分组统计:账单数量趋势(Key 为分组起始日期 yyyy-MM-dd)。 - /// - public Dictionary CountTrend { get; init; } = []; -} - -/// -/// 账单导出 DTO。 -/// -public sealed record BillingExportDto -{ - /// - /// 账单 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 账单编号。 - /// - public string StatementNo { get; init; } = string.Empty; - - /// - /// 关联订阅 ID(仅订阅/续费账单可能有值)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? SubscriptionId { get; init; } - - /// - /// 账单类型。 - /// - public BillingType BillingType { get; init; } - - /// - /// 计费周期开始时间(UTC)。 - /// - public DateTime PeriodStart { get; init; } - - /// - /// 计费周期结束时间(UTC)。 - /// - public DateTime PeriodEnd { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 折扣金额。 - /// - public decimal DiscountAmount { get; init; } - - /// - /// 税费金额。 - /// - public decimal TaxAmount { get; init; } - - /// - /// 总金额。 - /// - public decimal TotalAmount { get; init; } - - /// - /// 已付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 账单状态。 - /// - public TenantBillingStatus Status { get; init; } - - /// - /// 币种。 - /// - public string Currency { get; init; } = "CNY"; - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } - - /// - /// 账单明细列表。 - /// - public List LineItems { get; init; } = []; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingExportDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingExportDto.cs deleted file mode 100644 index 341d515..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingExportDto.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单导出 DTO。 -/// -public sealed record BillingExportDto -{ - /// - /// 账单 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 账单编号。 - /// - public string StatementNo { get; init; } = string.Empty; - - /// - /// 订阅 ID(可选)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? SubscriptionId { get; init; } - - /// - /// 账单类型。 - /// - public BillingType BillingType { get; init; } - - /// - /// 账单状态。 - /// - public TenantBillingStatus Status { get; init; } - - /// - /// 计费周期开始时间(UTC)。 - /// - public DateTime PeriodStart { get; init; } - - /// - /// 计费周期结束时间(UTC)。 - /// - public DateTime PeriodEnd { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 折扣金额。 - /// - public decimal DiscountAmount { get; init; } - - /// - /// 税费金额。 - /// - public decimal TaxAmount { get; init; } - - /// - /// 总金额(应付金额 - 折扣 + 税费)。 - /// - public decimal TotalAmount { get; init; } - - /// - /// 已支付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 币种。 - /// - public string Currency { get; init; } = "CNY"; - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 备注。 - /// - public string? Notes { get; init; } - - /// - /// 账单明细。 - /// - public IReadOnlyList LineItems { get; init; } = []; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingLineItemDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingLineItemDto.cs deleted file mode 100644 index 8be6cca..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingLineItemDto.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单明细行项目 DTO。 -/// -public sealed record BillingLineItemDto -{ - /// - /// 明细类型(如:订阅费、配额包费用、其他费用)。 - /// - public string ItemType { get; init; } = string.Empty; - - /// - /// 描述。 - /// - public string Description { get; init; } = string.Empty; - - /// - /// 数量。 - /// - public decimal Quantity { get; init; } - - /// - /// 单价。 - /// - public decimal UnitPrice { get; init; } - - /// - /// 金额(数量 × 单价)。 - /// - public decimal Amount { get; init; } - - /// - /// 折扣率(0-1,可选)。 - /// - public decimal? DiscountRate { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs deleted file mode 100644 index 2bea943..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingListDto.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单列表 DTO(管理员端列表展示)。 -/// -public sealed record BillingListDto -{ - /// - /// 账单 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 订阅 ID(可选)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? SubscriptionId { get; init; } - - /// - /// 账单编号。 - /// - public string StatementNo { get; init; } = string.Empty; - - /// - /// 计费周期开始时间(UTC)。 - /// - public DateTime PeriodStart { get; init; } - - /// - /// 计费周期结束时间(UTC)。 - /// - public DateTime PeriodEnd { get; init; } - - /// - /// 账单类型。 - /// - public BillingType BillingType { get; init; } - - /// - /// 账单状态。 - /// - public TenantBillingStatus Status { get; init; } - - /// - /// 应付金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 已支付金额。 - /// - public decimal AmountPaid { get; init; } - - /// - /// 折扣金额。 - /// - public decimal DiscountAmount { get; init; } - - /// - /// 税费金额。 - /// - public decimal TaxAmount { get; init; } - - /// - /// 总金额(应付金额 - 折扣 + 税费)。 - /// - public decimal TotalAmount { get; init; } - - /// - /// 币种。 - /// - public string Currency { get; init; } = "CNY"; - - /// - /// 到期日。 - /// - public DateTime DueDate { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 更新时间。 - /// - public DateTime? UpdatedAt { get; init; } - - /// - /// 是否逾期。 - /// - public bool IsOverdue { get; init; } - - /// - /// 逾期天数(未逾期为 0)。 - /// - public int OverdueDays { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs deleted file mode 100644 index fd00cb1..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingStatisticsDto.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单统计数据 DTO。 -/// -public sealed record BillingStatisticsDto -{ - /// - /// 租户 ID(可选,管理员可跨租户统计)。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? TenantId { get; init; } - - /// - /// 统计开始时间(UTC)。 - /// - public DateTime StartDate { get; init; } - - /// - /// 统计结束时间(UTC)。 - /// - public DateTime EndDate { get; init; } - - /// - /// 分组方式(Day/Week/Month)。 - /// - public string GroupBy { get; init; } = "Day"; - - /// - /// 总账单数量。 - /// - public int TotalCount { get; init; } - - /// - /// 待支付账单数量。 - /// - public int PendingCount { get; init; } - - /// - /// 已支付账单数量。 - /// - public int PaidCount { get; init; } - - /// - /// 逾期账单数量。 - /// - public int OverdueCount { get; init; } - - /// - /// 已取消账单数量。 - /// - public int CancelledCount { get; init; } - - /// - /// 总应收金额。 - /// - public decimal TotalAmountDue { get; init; } - - /// - /// 已收金额。 - /// - public decimal TotalAmountPaid { get; init; } - - /// - /// 未收金额。 - /// - public decimal TotalAmountUnpaid { get; init; } - - /// - /// 逾期金额。 - /// - public decimal TotalOverdueAmount { get; init; } - - /// - /// 应收金额趋势(Key 为日期桶字符串)。 - /// - public IReadOnlyDictionary AmountDueTrend { get; init; } = new Dictionary(); - - /// - /// 实收金额趋势(Key 为日期桶字符串)。 - /// - public IReadOnlyDictionary AmountPaidTrend { get; init; } = new Dictionary(); - - /// - /// 数量趋势(Key 为日期桶字符串)。 - /// - public IReadOnlyDictionary CountTrend { get; init; } = new Dictionary(); -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingTrendPointDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingTrendPointDto.cs deleted file mode 100644 index 68c8c9e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillingTrendPointDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 账单趋势数据点 DTO。 -/// -public sealed record BillingTrendPointDto -{ - /// - /// 分组时间点(Day/Week/Month 对齐后的时间)。 - /// - public DateTime Period { get; init; } - - /// - /// 账单数量。 - /// - public int Count { get; init; } - - /// - /// 应收金额。 - /// - public decimal AmountDue { get; init; } - - /// - /// 实收金额。 - /// - public decimal AmountPaid { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentDto.cs deleted file mode 100644 index 79f0921..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentDto.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 支付记录 DTO。 -/// -public sealed record PaymentDto -{ - /// - /// 支付记录 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 账单 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long BillingStatementId { get; init; } - - /// - /// 支付金额。 - /// - public decimal Amount { get; init; } - - /// - /// 支付方式。 - /// - public TenantPaymentMethod Method { get; init; } - - /// - /// 支付状态。 - /// - public TenantPaymentStatus Status { get; init; } - - /// - /// 交易号。 - /// - public string? TransactionNo { get; init; } - - /// - /// 支付凭证 URL。 - /// - public string? ProofUrl { get; init; } - - /// - /// 支付时间。 - /// - public DateTime? PaidAt { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } - - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs deleted file mode 100644 index 0c4dd58..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentRecordDto.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Billings.Dto; - -/// -/// 支付记录 DTO(管理员端)。 -/// -public sealed record PaymentRecordDto -{ - /// - /// 支付记录 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 关联的账单 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long BillingId { get; init; } - - /// - /// 支付金额。 - /// - public decimal Amount { get; init; } - - /// - /// 支付方式。 - /// - public TenantPaymentMethod Method { get; init; } - - /// - /// 支付状态。 - /// - public TenantPaymentStatus Status { get; init; } - - /// - /// 交易号。 - /// - public string? TransactionNo { get; init; } - - /// - /// 支付凭证 URL。 - /// - public string? ProofUrl { get; init; } - - /// - /// 支付时间。 - /// - public DateTime? PaidAt { get; init; } - - /// - /// 是否已审核。 - /// - public bool IsVerified { get; init; } - - /// - /// 审核人 ID。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? VerifiedBy { get; init; } - - /// - /// 审核时间。 - /// - public DateTime? VerifiedAt { get; init; } - - /// - /// 退款原因。 - /// - public string? RefundReason { get; init; } - - /// - /// 退款时间。 - /// - public DateTime? RefundedAt { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } - - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/BatchUpdateStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/BatchUpdateStatusCommandHandler.cs deleted file mode 100644 index d5d63ce..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/BatchUpdateStatusCommandHandler.cs +++ /dev/null @@ -1,88 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 批量更新账单状态处理器。 -/// -public sealed class BatchUpdateStatusCommandHandler( - ITenantBillingRepository billingRepository) - : IRequestHandler -{ - /// - /// 处理批量更新账单状态请求。 - /// - /// 批量更新状态命令。 - /// 取消标记。 - /// 成功更新的账单数量。 - public async Task Handle(BatchUpdateStatusCommand request, CancellationToken cancellationToken) - { - // 1. 参数验证 - if (request.BillingIds.Length == 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "账单 ID 列表不能为空"); - } - - // 2. 查询所有账单 - var billings = await billingRepository.GetByIdsAsync(request.BillingIds, cancellationToken); - if (billings.Count == 0) - { - throw new BusinessException(ErrorCodes.NotFound, "未找到任何匹配的账单"); - } - - // 3. 批量更新状态 - var now = DateTime.UtcNow; - var updatedCount = 0; - foreach (var billing in billings) - { - // 业务规则检查:某些状态转换可能不允许 - if (CanTransitionStatus(billing.Status, request.NewStatus)) - { - billing.Status = request.NewStatus; - billing.UpdatedAt = now; - - if (!string.IsNullOrWhiteSpace(request.Notes)) - { - billing.Notes = string.IsNullOrWhiteSpace(billing.Notes) - ? $"[批量操作] {request.Notes}" - : $"{billing.Notes}\n[批量操作] {request.Notes}"; - } - - await billingRepository.UpdateAsync(billing, cancellationToken); - updatedCount++; - } - } - - // 4. 持久化变更 - await billingRepository.SaveChangesAsync(cancellationToken); - - return updatedCount; - } - - /// - /// 检查状态转换是否允许。 - /// - private static bool CanTransitionStatus( - TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus currentStatus, - TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus newStatus) - { - // 已支付的账单不能改为其他状态 - if (currentStatus == TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Paid - && newStatus != TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Paid) - { - return false; - } - - // 已取消的账单不能改为其他状态 - if (currentStatus == TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Cancelled) - { - return false; - } - - return true; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CancelBillingCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CancelBillingCommandHandler.cs deleted file mode 100644 index c1a8da3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CancelBillingCommandHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 取消账单命令处理器。 -/// -public sealed class CancelBillingCommandHandler( - ITenantBillingRepository billingRepository) - : IRequestHandler -{ - /// - public async Task Handle(CancelBillingCommand request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var billing = await billingRepository.FindByIdAsync(request.BillingId, cancellationToken); - if (billing is null) - { - throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); - } - - // 2. 取消账单(领域规则校验在实体方法内) - billing.Cancel(request.Reason); - - // 3. 持久化 - await billingRepository.UpdateAsync(billing, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - return Unit.Value; - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ConfirmPaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ConfirmPaymentCommandHandler.cs deleted file mode 100644 index 57a36d1..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ConfirmPaymentCommandHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.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.Billings.Handlers; - -/// -/// 一键确认收款处理器(记录支付 + 立即审核通过 + 同步更新账单已收金额/状态)。 -/// -public sealed class ConfirmPaymentCommandHandler( - ITenantBillingRepository billingRepository, - ITenantPaymentRepository paymentRepository, - IIdGenerator idGenerator, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(ConfirmPaymentCommand request, CancellationToken cancellationToken) - { - // 1. 校验操作者身份(用于写入 VerifiedBy) - if (!currentUserAccessor.IsAuthenticated || currentUserAccessor.UserId <= 0) - { - throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份"); - } - - // 2. 查询账单 - var billing = await billingRepository.FindByIdAsync(request.BillingId, cancellationToken); - if (billing is null) - { - throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); - } - - // 3. 业务规则检查 - if (billing.Status == TenantBillingStatus.Paid) - { - throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款"); - } - - if (billing.Status == TenantBillingStatus.Cancelled) - { - throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款"); - } - - // 4. 金额边界:不允许超过剩余应收(与前端校验保持一致) - var totalAmount = billing.CalculateTotalAmount(); - var remainingAmount = totalAmount - billing.AmountPaid; - if (request.Amount > remainingAmount) - { - throw new BusinessException(ErrorCodes.BadRequest, "支付金额不能超过剩余应收"); - } - - // 5. 幂等校验:交易号唯一 - if (!string.IsNullOrWhiteSpace(request.TransactionNo)) - { - var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken); - if (exists is not null) - { - throw new BusinessException(ErrorCodes.Conflict, "交易号已存在,疑似重复提交"); - } - } - - // 6. 构建支付记录并立即审核通过 - var now = DateTime.UtcNow; - var payment = new TenantPayment - { - Id = idGenerator.NextId(), - TenantId = billing.TenantId, - BillingStatementId = request.BillingId, - Amount = request.Amount, - Method = request.Method, - Status = TenantPaymentStatus.Pending, - TransactionNo = string.IsNullOrWhiteSpace(request.TransactionNo) ? null : request.TransactionNo.Trim(), - ProofUrl = request.ProofUrl, - PaidAt = now, - Notes = request.Notes - }; - - payment.Verify(currentUserAccessor.UserId); - - // 7. 同步更新账单已收金额/状态(支持分次收款) - billing.MarkAsPaid(payment.Amount, payment.TransactionNo ?? string.Empty); - - // 8. 持久化变更(同一 DbContext 下单次 SaveChanges 可提交两张表) - await paymentRepository.AddAsync(payment, cancellationToken); - await billingRepository.UpdateAsync(billing, cancellationToken); - await paymentRepository.SaveChangesAsync(cancellationToken); - - // 9. 返回 DTO - return payment.ToPaymentRecordDto(); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillCommandHandler.cs deleted file mode 100644 index bbcf044..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillCommandHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.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.Billings.Handlers; - -/// -/// 创建账单处理器。 -/// -public sealed class CreateBillCommandHandler( - ITenantBillingRepository billingRepository, - ITenantRepository tenantRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - /// 处理创建账单请求。 - /// - /// 创建命令。 - /// 取消标记。 - /// 账单 DTO。 - public async Task Handle(CreateBillCommand request, CancellationToken cancellationToken) - { - // 1. 验证租户存在 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken); - if (tenant is null) - { - throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - } - - // 2. 生成账单编号 - var statementNo = $"BILL-{DateTime.UtcNow:yyyyMMdd}-{idGenerator.NextId()}"; - - // 3. 构建账单实体 - var bill = new TenantBillingStatement - { - TenantId = request.TenantId, - StatementNo = statementNo, - PeriodStart = DateTime.UtcNow, - PeriodEnd = DateTime.UtcNow, - AmountDue = request.AmountDue, - AmountPaid = 0, - Status = TenantBillingStatus.Pending, - DueDate = request.DueDate, - LineItemsJson = request.Notes - }; - - // 4. 持久化账单 - await billingRepository.AddAsync(bill, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - // 5. 返回 DTO - return bill.ToDto(tenant.Name); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillingCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillingCommandHandler.cs deleted file mode 100644 index 8c8727d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillingCommandHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using MediatR; -using System.Text.Json; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.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.Billings.Handlers; - -/// -/// 创建账单命令处理器。 -/// -public sealed class CreateBillingCommandHandler( - ITenantRepository tenantRepository, - ITenantBillingRepository billingRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(CreateBillingCommand request, CancellationToken cancellationToken) - { - // 1. 校验租户存在 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken); - if (tenant is null) - { - throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - } - - // 2. 构建账单实体 - var now = DateTime.UtcNow; - var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}"; - var lineItemsJson = JsonSerializer.Serialize(request.LineItems); - - var billing = new TenantBillingStatement - { - TenantId = request.TenantId, - StatementNo = statementNo, - BillingType = request.BillingType, - SubscriptionId = null, - PeriodStart = now, - PeriodEnd = now, - AmountDue = request.AmountDue, - DiscountAmount = 0m, - TaxAmount = 0m, - AmountPaid = 0m, - Currency = "CNY", - Status = TenantBillingStatus.Pending, - DueDate = request.DueDate, - LineItemsJson = lineItemsJson, - Notes = request.Notes - }; - - // 3. 持久化账单 - await billingRepository.AddAsync(billing, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - // 4. 返回详情 DTO - return billing.ToBillingDetailDto([], tenant.Name); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ExportBillingsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ExportBillingsQueryHandler.cs deleted file mode 100644 index 7731d80..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ExportBillingsQueryHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Domain.Tenants.Services; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 导出账单处理器。 -/// -public sealed class ExportBillingsQueryHandler( - ITenantBillingRepository billingRepository, - IBillingExportService exportService) - : IRequestHandler -{ - /// - public async Task Handle(ExportBillingsQuery request, CancellationToken cancellationToken) - { - // 1. 参数验证 - if (request.BillingIds.Length == 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "账单 ID 列表不能为空"); - } - - // 2. 查询账单数据 - var billings = await billingRepository.GetByIdsAsync(request.BillingIds, cancellationToken); - if (billings.Count == 0) - { - throw new BusinessException(ErrorCodes.NotFound, "未找到任何匹配的账单"); - } - - // 3. 根据格式导出 - var format = (request.Format ?? string.Empty).Trim().ToLowerInvariant(); - return format switch - { - "excel" or "xlsx" => await exportService.ExportToExcelAsync(billings, cancellationToken), - "pdf" => await exportService.ExportToPdfAsync(billings, cancellationToken), - "csv" => await exportService.ExportToCsvAsync(billings, cancellationToken), - _ => throw new BusinessException(ErrorCodes.BadRequest, $"不支持的导出格式: {request.Format}") - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GenerateSubscriptionBillingCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GenerateSubscriptionBillingCommandHandler.cs deleted file mode 100644 index d92b089..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GenerateSubscriptionBillingCommandHandler.cs +++ /dev/null @@ -1,102 +0,0 @@ -using MediatR; -using System.Text.Json; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.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.Billings.Handlers; - -/// -/// 生成订阅账单命令处理器。 -/// -public sealed class GenerateSubscriptionBillingCommandHandler( - ISubscriptionRepository subscriptionRepository, - ITenantBillingRepository billingRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(GenerateSubscriptionBillingCommand request, CancellationToken cancellationToken) - { - // 1. 查询订阅详情(含租户/套餐信息) - var detail = await subscriptionRepository.GetDetailAsync(request.SubscriptionId, cancellationToken); - if (detail is null) - { - throw new BusinessException(ErrorCodes.NotFound, "订阅不存在"); - } - - // 2. 校验套餐价格信息 - var subscription = detail.Subscription; - var package = detail.Package; - if (package is null) - { - throw new BusinessException(ErrorCodes.BusinessError, "订阅未关联有效套餐,无法生成账单"); - } - - // 3. 按订阅周期选择价格(简化规则:优先按年/按月) - var billingPeriodDays = (subscription.EffectiveTo - subscription.EffectiveFrom).TotalDays; - var amountDue = billingPeriodDays >= 300 - ? package.YearlyPrice - : package.MonthlyPrice; - - if (!amountDue.HasValue) - { - throw new BusinessException(ErrorCodes.BusinessError, "套餐价格未配置,无法生成账单"); - } - - // 4. 幂等校验:同一周期开始时间仅允许存在一张未取消账单 - var exists = await billingRepository.ExistsNotCancelledByPeriodStartAsync(subscription.TenantId, subscription.EffectiveFrom, cancellationToken); - if (exists) - { - throw new BusinessException(ErrorCodes.Conflict, "该订阅周期的账单已存在"); - } - - // 5. 构建账单实体 - var now = DateTime.UtcNow; - var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}"; - var lineItems = new List - { - new() - { - ItemType = "Subscription", - Description = $"套餐 {package.Name} 订阅费用", - Quantity = 1, - UnitPrice = amountDue.Value, - Amount = amountDue.Value, - DiscountRate = null - } - }; - - var billing = new TenantBillingStatement - { - TenantId = subscription.TenantId, - StatementNo = statementNo, - BillingType = BillingType.Subscription, - SubscriptionId = subscription.Id, - PeriodStart = subscription.EffectiveFrom, - PeriodEnd = subscription.EffectiveTo, - AmountDue = amountDue.Value, - DiscountAmount = 0m, - TaxAmount = 0m, - AmountPaid = 0m, - Currency = "CNY", - Status = TenantBillingStatus.Pending, - DueDate = now.AddDays(7), - LineItemsJson = JsonSerializer.Serialize(lineItems), - Notes = subscription.Notes - }; - - // 6. 持久化账单 - await billingRepository.AddAsync(billing, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - // 7. 返回详情 DTO - return billing.ToBillingDetailDto([], detail.TenantName); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillDetailQueryHandler.cs deleted file mode 100644 index c368fac..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillDetailQueryHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 获取账单详情查询处理器。 -/// -public sealed class GetBillDetailQueryHandler( - ITenantBillingRepository billingRepository, - ITenantPaymentRepository paymentRepository, - ITenantRepository tenantRepository) - : IRequestHandler -{ - /// - /// 处理获取账单详情请求。 - /// - /// 查询请求。 - /// 取消标记。 - /// 账单详情或 null。 - public async Task Handle(GetBillDetailQuery request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken); - if (bill is null) - { - return null; - } - - // 2. 查询支付记录 - var payments = await paymentRepository.GetByBillingIdAsync(request.BillId, cancellationToken); - - // 3. 查询租户名称 - var tenant = await tenantRepository.FindByIdAsync(bill.TenantId, cancellationToken); - - // 4. 返回详情 DTO - return bill.ToDetailDto(payments.ToList(), tenant?.Name); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillListQueryHandler.cs deleted file mode 100644 index 51782d5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillListQueryHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 获取账单列表查询处理器。 -/// -public sealed class GetBillListQueryHandler( - ITenantBillingRepository billingRepository, - ITenantRepository tenantRepository) - : IRequestHandler> -{ - /// - /// 处理获取账单列表请求。 - /// - /// 查询请求。 - /// 取消标记。 - /// 分页账单列表。 - public async Task> Handle(GetBillListQuery request, CancellationToken cancellationToken) - { - // 1. 分页查询账单 - var (bills, total) = await billingRepository.SearchPagedAsync( - request.TenantId, - request.Status, - request.StartDate, - request.EndDate, - null, - null, - request.Keyword, - request.PageNumber, - request.PageSize, - cancellationToken); - - // 2. 无数据直接返回 - if (bills.Count == 0) - { - return new PagedResult([], request.PageNumber, request.PageSize, total); - } - - // 3. 批量查询租户信息 - var tenantIds = bills.Select(b => b.TenantId).Distinct().ToArray(); - var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken); - var tenantDict = tenants.ToDictionary(t => t.Id, t => t.Name); - - // 4. 映射 DTO - var result = bills.Select(b => b.ToDto(tenantDict.GetValueOrDefault(b.TenantId))).ToList(); - - // 5. 返回分页结果 - return new PagedResult(result, request.PageNumber, request.PageSize, total); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingDetailQueryHandler.cs deleted file mode 100644 index cb9ae96..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingDetailQueryHandler.cs +++ /dev/null @@ -1,236 +0,0 @@ -using MediatR; -using System.Data; -using System.Data.Common; -using System.Text.Json; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 查询账单详情处理器。 -/// -public sealed class GetBillingDetailQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler -{ - /// - /// 处理查询账单详情请求。 - /// - /// 查询命令。 - /// 取消标记。 - /// 账单详情 DTO。 - public async Task Handle(GetBillingDetailQuery request, CancellationToken cancellationToken) - { - // 1. 查询账单 + 支付记录(同一连接,避免多次往返) - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 1.1 查询账单 - await using var billCommand = CreateCommand( - connection, - BuildBillingSql(), - [ - ("billingId", request.BillingId) - ]); - - await using var billReader = await billCommand.ExecuteReaderAsync(token); - if (!await billReader.ReadAsync(token)) - { - throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); - } - - // 1.2 读取账单行数据到内存(释放 Reader,避免同连接并发执行命令) - var billingId = billReader.GetInt64(0); - var tenantId = billReader.GetInt64(1); - var tenantName = billReader.IsDBNull(2) ? string.Empty : billReader.GetString(2); - long? subscriptionId = billReader.IsDBNull(3) ? null : billReader.GetInt64(3); - var statementNo = billReader.GetString(4); - var billingType = (BillingType)billReader.GetInt32(5); - var status = (TenantBillingStatus)billReader.GetInt32(6); - var periodStart = billReader.GetDateTime(7); - var periodEnd = billReader.GetDateTime(8); - var amountDue = billReader.GetDecimal(9); - var discountAmount = billReader.GetDecimal(10); - var taxAmount = billReader.GetDecimal(11); - var amountPaid = billReader.GetDecimal(12); - var currency = billReader.IsDBNull(13) ? "CNY" : billReader.GetString(13); - var dueDate = billReader.GetDateTime(14); - DateTime? reminderSentAt = billReader.IsDBNull(15) ? null : billReader.GetDateTime(15); - DateTime? overdueNotifiedAt = billReader.IsDBNull(16) ? null : billReader.GetDateTime(16); - var notes = billReader.IsDBNull(17) ? null : billReader.GetString(17); - var lineItemsJson = billReader.IsDBNull(18) ? null : billReader.GetString(18); - var createdAt = billReader.GetDateTime(19); - long? createdBy = billReader.IsDBNull(20) ? null : billReader.GetInt64(20); - DateTime? updatedAt = billReader.IsDBNull(21) ? null : billReader.GetDateTime(21); - long? updatedBy = billReader.IsDBNull(22) ? null : billReader.GetInt64(22); - - // 1.3 主动释放账单 Reader,确保后续查询不会触发 Npgsql 并发命令异常 - await billReader.DisposeAsync(); - - // 1.4 反序列化账单明细 - var lineItems = new List(); - if (!string.IsNullOrWhiteSpace(lineItemsJson)) - { - try - { - lineItems = JsonSerializer.Deserialize>(lineItemsJson) ?? []; - } - catch - { - lineItems = []; - } - } - - // 1.5 查询支付记录 - var payments = new List(); - await using var paymentCommand = CreateCommand( - connection, - BuildPaymentsSql(), - [ - ("billingId", request.BillingId) - ]); - - await using var paymentReader = await paymentCommand.ExecuteReaderAsync(token); - while (await paymentReader.ReadAsync(token)) - { - payments.Add(new PaymentRecordDto - { - Id = paymentReader.GetInt64(0), - TenantId = paymentReader.GetInt64(1), - BillingId = paymentReader.GetInt64(2), - Amount = paymentReader.GetDecimal(3), - Method = (TenantPaymentMethod)paymentReader.GetInt32(4), - Status = (TenantPaymentStatus)paymentReader.GetInt32(5), - TransactionNo = paymentReader.IsDBNull(6) ? null : paymentReader.GetString(6), - ProofUrl = paymentReader.IsDBNull(7) ? null : paymentReader.GetString(7), - Notes = paymentReader.IsDBNull(8) ? null : paymentReader.GetString(8), - VerifiedBy = paymentReader.IsDBNull(9) ? null : paymentReader.GetInt64(9), - VerifiedAt = paymentReader.IsDBNull(10) ? null : paymentReader.GetDateTime(10), - RefundReason = paymentReader.IsDBNull(11) ? null : paymentReader.GetString(11), - RefundedAt = paymentReader.IsDBNull(12) ? null : paymentReader.GetDateTime(12), - PaidAt = paymentReader.IsDBNull(13) ? null : paymentReader.GetDateTime(13), - IsVerified = !paymentReader.IsDBNull(10), - CreatedAt = paymentReader.GetDateTime(14) - }); - } - - // 1.6 组装详情 DTO - var totalAmount = amountDue - discountAmount + taxAmount; - - return new BillingDetailDto - { - Id = billingId, - TenantId = tenantId, - TenantName = tenantName, - SubscriptionId = subscriptionId, - StatementNo = statementNo, - BillingType = billingType, - Status = status, - PeriodStart = periodStart, - PeriodEnd = periodEnd, - AmountDue = amountDue, - DiscountAmount = discountAmount, - TaxAmount = taxAmount, - TotalAmount = totalAmount, - AmountPaid = amountPaid, - Currency = currency, - DueDate = dueDate, - ReminderSentAt = reminderSentAt, - OverdueNotifiedAt = overdueNotifiedAt, - LineItemsJson = lineItemsJson, - LineItems = lineItems, - Payments = payments, - Notes = notes, - CreatedAt = createdAt, - CreatedBy = createdBy, - UpdatedAt = updatedAt, - UpdatedBy = updatedBy - }; - }, - cancellationToken); - } - - private static string BuildBillingSql() - { - return """ - select - b."Id", - b."TenantId", - t."Name" as "TenantName", - b."SubscriptionId", - b."StatementNo", - b."BillingType", - b."Status", - b."PeriodStart", - b."PeriodEnd", - b."AmountDue", - b."DiscountAmount", - b."TaxAmount", - b."AmountPaid", - b."Currency", - b."DueDate", - b."ReminderSentAt", - b."OverdueNotifiedAt", - b."Notes", - b."LineItemsJson", - b."CreatedAt", - b."CreatedBy", - b."UpdatedAt", - b."UpdatedBy" - from public.tenant_billing_statements b - join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null - where b."DeletedAt" is null - and b."Id" = @billingId - limit 1; - """; - } - - private static string BuildPaymentsSql() - { - return """ - select - p."Id", - p."TenantId", - p."BillingStatementId", - p."Amount", - p."Method", - p."Status", - p."TransactionNo", - p."ProofUrl", - p."Notes", - p."VerifiedBy", - p."VerifiedAt", - p."RefundReason", - p."RefundedAt", - p."PaidAt", - p."CreatedAt" - from public.tenant_payments p - where p."DeletedAt" is null - and p."BillingStatementId" = @billingId - order by p."CreatedAt" desc; - """; - } - - 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/Billings/Handlers/GetBillingListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingListQueryHandler.cs deleted file mode 100644 index 894e644..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingListQueryHandler.cs +++ /dev/null @@ -1,233 +0,0 @@ -using MediatR; -using System.Data; -using System.Data.Common; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 分页查询账单列表处理器。 -/// -public sealed class GetBillingListQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - /// 处理分页查询账单列表请求。 - /// - /// 查询命令。 - /// 取消标记。 - /// 分页账单列表 DTO。 - public async Task> Handle(GetBillingListQuery request, CancellationToken cancellationToken) - { - // 1. 参数规范化 - var page = request.PageNumber <= 0 ? 1 : request.PageNumber; - var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize; - var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim(); - var minAmount = request.MinAmount; - var maxAmount = request.MaxAmount; - var offset = (page - 1) * pageSize; - - // 1.1 金额区间规范化(避免 min > max 导致结果为空) - if (minAmount.HasValue && maxAmount.HasValue && minAmount.Value > maxAmount.Value) - { - (minAmount, maxAmount) = (maxAmount, minAmount); - } - - // 2. 排序白名单(防 SQL 注入) - var orderBy = request.SortBy?.Trim() switch - { - "DueDate" => "b.\"DueDate\"", - "AmountDue" => "b.\"AmountDue\"", - "PeriodStart" => "b.\"PeriodStart\"", - "PeriodEnd" => "b.\"PeriodEnd\"", - "CreatedAt" => "b.\"CreatedAt\"", - _ => "b.\"CreatedAt\"" - }; - - // 3. 查询总数 + 列表 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 3.1 统计总数 - var total = await ExecuteScalarIntAsync( - connection, - BuildCountSql(), - [ - ("tenantId", request.TenantId), - ("status", request.Status.HasValue ? (int)request.Status.Value : null), - ("billingType", request.BillingType.HasValue ? (int)request.BillingType.Value : null), - ("startDate", request.StartDate), - ("endDate", request.EndDate), - ("minAmount", minAmount), - ("maxAmount", maxAmount), - ("keyword", keyword) - ], - token); - - // 3.2 查询列表 - var listSql = BuildListSql(orderBy, request.SortDesc); - await using var listCommand = CreateCommand( - connection, - listSql, - [ - ("tenantId", request.TenantId), - ("status", request.Status.HasValue ? (int)request.Status.Value : null), - ("billingType", request.BillingType.HasValue ? (int)request.BillingType.Value : null), - ("startDate", request.StartDate), - ("endDate", request.EndDate), - ("minAmount", minAmount), - ("maxAmount", maxAmount), - ("keyword", keyword), - ("offset", offset), - ("limit", pageSize) - ]); - - await using var reader = await listCommand.ExecuteReaderAsync(token); - var now = DateTime.UtcNow; - var items = new List(); - while (await reader.ReadAsync(token)) - { - var dueDate = reader.GetDateTime(13); - var status = (TenantBillingStatus)reader.GetInt32(12); - var amountDue = reader.GetDecimal(8); - var discountAmount = reader.GetDecimal(9); - var taxAmount = reader.GetDecimal(10); - var totalAmount = amountDue - discountAmount + taxAmount; - - // 3.2.1 逾期辅助字段 - var isOverdue = status is TenantBillingStatus.Overdue - || (status is TenantBillingStatus.Pending && dueDate < now); - var overdueDays = dueDate < now ? (int)(now - dueDate).TotalDays : 0; - - items.Add(new BillingListDto - { - Id = reader.GetInt64(0), - TenantId = reader.GetInt64(1), - TenantName = reader.IsDBNull(2) ? string.Empty : reader.GetString(2), - SubscriptionId = reader.IsDBNull(3) ? null : reader.GetInt64(3), - StatementNo = reader.GetString(4), - BillingType = (BillingType)reader.GetInt32(5), - PeriodStart = reader.GetDateTime(6), - PeriodEnd = reader.GetDateTime(7), - AmountDue = amountDue, - DiscountAmount = discountAmount, - TaxAmount = taxAmount, - TotalAmount = totalAmount, - AmountPaid = reader.GetDecimal(11), - Currency = reader.IsDBNull(14) ? "CNY" : reader.GetString(14), - Status = status, - DueDate = dueDate, - IsOverdue = isOverdue, - OverdueDays = overdueDays, - CreatedAt = reader.GetDateTime(15), - UpdatedAt = reader.IsDBNull(16) ? null : reader.GetDateTime(16) - }); - } - - // 3.3 返回分页 - return new PagedResult(items, page, pageSize, total); - }, - cancellationToken); - } - - private static string BuildCountSql() - { - return """ - select count(*) - from public.tenant_billing_statements b - join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null - where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) - and (@status::int is null or b."Status" = @status) - and (@billingType::int is null or b."BillingType" = @billingType) - and (@startDate::timestamp with time zone is null or b."PeriodStart" >= @startDate) - and (@endDate::timestamp with time zone is null or b."PeriodEnd" <= @endDate) - and (@minAmount::numeric is null or b."AmountDue" >= @minAmount) - and (@maxAmount::numeric is null or b."AmountDue" <= @maxAmount) - and ( - @keyword::text is null - or b."StatementNo" ilike ('%' || @keyword::text || '%') - or t."Name" ilike ('%' || @keyword::text || '%') - ); - """; - } - - private static string BuildListSql(string orderBy, bool sortDesc) - { - var direction = sortDesc ? "desc" : "asc"; - - return $""" - select - b."Id", - b."TenantId", - t."Name" as "TenantName", - b."SubscriptionId", - b."StatementNo", - b."BillingType", - b."PeriodStart", - b."PeriodEnd", - b."AmountDue", - b."DiscountAmount", - b."TaxAmount", - b."AmountPaid", - b."Status", - b."DueDate", - b."Currency", - b."CreatedAt", - b."UpdatedAt" - from public.tenant_billing_statements b - join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null - where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) - and (@status::int is null or b."Status" = @status) - and (@billingType::int is null or b."BillingType" = @billingType) - and (@startDate::timestamp with time zone is null or b."PeriodStart" >= @startDate) - and (@endDate::timestamp with time zone is null or b."PeriodEnd" <= @endDate) - and (@minAmount::numeric is null or b."AmountDue" >= @minAmount) - and (@maxAmount::numeric is null or b."AmountDue" <= @maxAmount) - and ( - @keyword::text is null - or b."StatementNo" ilike ('%' || @keyword::text || '%') - or t."Name" ilike ('%' || @keyword::text || '%') - ) - 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; - - 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/Billings/Handlers/GetBillingPaymentsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingPaymentsQueryHandler.cs deleted file mode 100644 index b7df3b5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingPaymentsQueryHandler.cs +++ /dev/null @@ -1,139 +0,0 @@ -using MediatR; -using System.Data; -using System.Data.Common; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 查询账单支付记录处理器。 -/// -public sealed class GetBillingPaymentsQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - /// 处理查询账单支付记录请求。 - /// - /// 查询命令。 - /// 取消标记。 - /// 支付记录列表 DTO。 - public async Task> Handle(GetBillingPaymentsQuery request, CancellationToken cancellationToken) - { - // 1. 校验账单是否存在 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 1.1 校验账单存在 - var exists = await ExecuteScalarIntAsync( - connection, - """ - select 1 - from public.tenant_billing_statements b - where b."DeletedAt" is null - and b."Id" = @billingId - limit 1; - """, - [ - ("billingId", request.BillingId) - ], - token); - - if (exists == 0) - { - throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); - } - - // 1.2 查询支付记录 - await using var command = CreateCommand( - connection, - """ - select - p."Id", - p."TenantId", - p."BillingStatementId", - p."Amount", - p."Method", - p."Status", - p."TransactionNo", - p."ProofUrl", - p."Notes", - p."VerifiedBy", - p."VerifiedAt", - p."RefundReason", - p."RefundedAt", - p."PaidAt", - p."CreatedAt" - from public.tenant_payments p - where p."DeletedAt" is null - and p."BillingStatementId" = @billingId - order by p."CreatedAt" desc; - """, - [ - ("billingId", request.BillingId) - ]); - - await using var reader = await command.ExecuteReaderAsync(token); - var results = new List(); - while (await reader.ReadAsync(token)) - { - results.Add(new PaymentRecordDto - { - Id = reader.GetInt64(0), - TenantId = reader.GetInt64(1), - BillingId = reader.GetInt64(2), - Amount = reader.GetDecimal(3), - Method = (TenantPaymentMethod)reader.GetInt32(4), - Status = (TenantPaymentStatus)reader.GetInt32(5), - TransactionNo = reader.IsDBNull(6) ? null : reader.GetString(6), - ProofUrl = reader.IsDBNull(7) ? null : reader.GetString(7), - Notes = reader.IsDBNull(8) ? null : reader.GetString(8), - VerifiedBy = reader.IsDBNull(9) ? null : reader.GetInt64(9), - VerifiedAt = reader.IsDBNull(10) ? null : reader.GetDateTime(10), - RefundReason = reader.IsDBNull(11) ? null : reader.GetString(11), - RefundedAt = reader.IsDBNull(12) ? null : reader.GetDateTime(12), - PaidAt = reader.IsDBNull(13) ? null : reader.GetDateTime(13), - IsVerified = !reader.IsDBNull(10), - CreatedAt = reader.GetDateTime(14) - }); - } - - return results; - }, - cancellationToken); - } - - 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/Billings/Handlers/GetBillingStatisticsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingStatisticsQueryHandler.cs deleted file mode 100644 index e5e3aa3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillingStatisticsQueryHandler.cs +++ /dev/null @@ -1,187 +0,0 @@ -using MediatR; -using System.Data; -using System.Data.Common; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 查询账单统计数据处理器。 -/// -public sealed class GetBillingStatisticsQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler -{ - /// - /// 处理查询账单统计数据请求。 - /// - /// 查询命令。 - /// 取消标记。 - /// 账单统计数据 DTO。 - public async Task Handle(GetBillingStatisticsQuery request, CancellationToken cancellationToken) - { - // 1. 参数规范化 - var startDate = request.StartDate ?? DateTime.UtcNow.AddMonths(-1); - var endDate = request.EndDate ?? DateTime.UtcNow; - var groupBy = NormalizeGroupBy(request.GroupBy); - - // 2. 查询统计数据(总览 + 趋势) - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 2.1 总览统计 - await using var summaryCommand = CreateCommand( - connection, - BuildSummarySql(), - [ - ("tenantId", request.TenantId), - ("startDate", startDate), - ("endDate", endDate), - ("now", DateTime.UtcNow) - ]); - - await using var summaryReader = await summaryCommand.ExecuteReaderAsync(token); - await summaryReader.ReadAsync(token); - - var totalCount = summaryReader.IsDBNull(0) ? 0 : summaryReader.GetInt32(0); - var pendingCount = summaryReader.IsDBNull(1) ? 0 : summaryReader.GetInt32(1); - var paidCount = summaryReader.IsDBNull(2) ? 0 : summaryReader.GetInt32(2); - var overdueCount = summaryReader.IsDBNull(3) ? 0 : summaryReader.GetInt32(3); - var cancelledCount = summaryReader.IsDBNull(4) ? 0 : summaryReader.GetInt32(4); - var totalAmountDue = summaryReader.IsDBNull(5) ? 0m : summaryReader.GetDecimal(5); - var totalAmountPaid = summaryReader.IsDBNull(6) ? 0m : summaryReader.GetDecimal(6); - var totalAmountUnpaid = summaryReader.IsDBNull(7) ? 0m : summaryReader.GetDecimal(7); - var totalOverdueAmount = summaryReader.IsDBNull(8) ? 0m : summaryReader.GetDecimal(8); - - // 2.2 趋势数据 - await using var trendCommand = CreateCommand( - connection, - BuildTrendSql(groupBy), - [ - ("tenantId", request.TenantId), - ("startDate", startDate), - ("endDate", endDate) - ]); - - await using var trendReader = await trendCommand.ExecuteReaderAsync(token); - var amountDueTrend = new Dictionary(); - var amountPaidTrend = new Dictionary(); - var countTrend = new Dictionary(); - while (await trendReader.ReadAsync(token)) - { - var bucket = trendReader.GetDateTime(0); - var key = bucket.ToString("yyyy-MM-dd"); - - amountDueTrend[key] = trendReader.IsDBNull(1) ? 0m : trendReader.GetDecimal(1); - amountPaidTrend[key] = trendReader.IsDBNull(2) ? 0m : trendReader.GetDecimal(2); - countTrend[key] = trendReader.IsDBNull(3) ? 0 : trendReader.GetInt32(3); - } - - // 2.3 组装 DTO - return new BillingStatisticsDto - { - TenantId = request.TenantId, - StartDate = startDate, - EndDate = endDate, - GroupBy = groupBy, - TotalCount = totalCount, - PendingCount = pendingCount, - PaidCount = paidCount, - OverdueCount = overdueCount, - CancelledCount = cancelledCount, - TotalAmountDue = totalAmountDue, - TotalAmountPaid = totalAmountPaid, - TotalAmountUnpaid = totalAmountUnpaid, - TotalOverdueAmount = totalOverdueAmount, - AmountDueTrend = amountDueTrend, - AmountPaidTrend = amountPaidTrend, - CountTrend = countTrend - }; - }, - cancellationToken); - } - - private static string NormalizeGroupBy(string? groupBy) - { - return groupBy?.Trim() switch - { - "Week" => "Week", - "Month" => "Month", - _ => "Day" - }; - } - - private static string BuildSummarySql() - { - return """ - select - count(*)::int as "TotalCount", - coalesce(sum(case when b."Status" = 0 then 1 else 0 end), 0)::int as "PendingCount", - coalesce(sum(case when b."Status" = 1 then 1 else 0 end), 0)::int as "PaidCount", - coalesce(sum(case when b."Status" = 2 then 1 else 0 end), 0)::int as "OverdueCount", - coalesce(sum(case when b."Status" = 3 then 1 else 0 end), 0)::int as "CancelledCount", - coalesce(sum(b."AmountDue"), 0)::numeric as "TotalAmountDue", - coalesce(sum(b."AmountPaid"), 0)::numeric as "TotalAmountPaid", - coalesce(sum((b."AmountDue" - b."DiscountAmount" + b."TaxAmount") - b."AmountPaid"), 0)::numeric as "TotalAmountUnpaid", - coalesce(sum( - case - when b."Status" in (0, 2) and b."DueDate" < @now - then (b."AmountDue" - b."DiscountAmount" + b."TaxAmount") - b."AmountPaid" - else 0 - end - ), 0)::numeric as "TotalOverdueAmount" - from public.tenant_billing_statements b - where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) - and b."PeriodStart" >= @startDate - and b."PeriodEnd" <= @endDate; - """; - } - - private static string BuildTrendSql(string groupBy) - { - var dateTrunc = groupBy switch - { - "Week" => "week", - "Month" => "month", - _ => "day" - }; - - return $""" - select - date_trunc('{dateTrunc}', b."PeriodStart") as "Bucket", - coalesce(sum(b."AmountDue"), 0)::numeric as "AmountDue", - coalesce(sum(b."AmountPaid"), 0)::numeric as "AmountPaid", - count(*)::int as "Count" - from public.tenant_billing_statements b - where b."DeletedAt" is null - and (@tenantId::bigint is null or b."TenantId" = @tenantId) - and b."PeriodStart" >= @startDate - and b."PeriodEnd" <= @endDate - group by 1 - order by 1 asc; - """; - } - - 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/Billings/Handlers/GetOverdueBillingsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetOverdueBillingsQueryHandler.cs deleted file mode 100644 index b75fecd..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetOverdueBillingsQueryHandler.cs +++ /dev/null @@ -1,172 +0,0 @@ -using MediatR; -using System.Data; -using System.Data.Common; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 查询逾期账单列表处理器。 -/// -public sealed class GetOverdueBillingsQueryHandler( - IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - /// 处理查询逾期账单列表请求。 - /// - /// 查询命令。 - /// 取消标记。 - /// 分页逾期账单列表 DTO。 - public async Task> Handle(GetOverdueBillingsQuery request, CancellationToken cancellationToken) - { - // 1. 参数规范化 - var page = request.PageNumber <= 0 ? 1 : request.PageNumber; - var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize; - var offset = (page - 1) * pageSize; - var now = DateTime.UtcNow; - - // 2. 查询总数 + 列表 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 2.1 统计总数 - var total = await ExecuteScalarIntAsync( - connection, - BuildCountSql(), - [ - ("now", now) - ], - token); - - // 2.2 查询列表 - await using var listCommand = CreateCommand( - connection, - BuildListSql(), - [ - ("now", now), - ("offset", offset), - ("limit", pageSize) - ]); - - await using var reader = await listCommand.ExecuteReaderAsync(token); - var items = new List(); - while (await reader.ReadAsync(token)) - { - var dueDate = reader.GetDateTime(13); - var status = (TenantBillingStatus)reader.GetInt32(12); - var amountDue = reader.GetDecimal(8); - var discountAmount = reader.GetDecimal(9); - var taxAmount = reader.GetDecimal(10); - var totalAmount = amountDue - discountAmount + taxAmount; - var overdueDays = dueDate < now ? (int)(now - dueDate).TotalDays : 0; - - items.Add(new BillingListDto - { - Id = reader.GetInt64(0), - TenantId = reader.GetInt64(1), - TenantName = reader.IsDBNull(2) ? string.Empty : reader.GetString(2), - SubscriptionId = reader.IsDBNull(3) ? null : reader.GetInt64(3), - StatementNo = reader.GetString(4), - BillingType = (BillingType)reader.GetInt32(5), - PeriodStart = reader.GetDateTime(6), - PeriodEnd = reader.GetDateTime(7), - AmountDue = amountDue, - DiscountAmount = discountAmount, - TaxAmount = taxAmount, - TotalAmount = totalAmount, - AmountPaid = reader.GetDecimal(11), - Status = status, - DueDate = dueDate, - Currency = reader.IsDBNull(14) ? "CNY" : reader.GetString(14), - IsOverdue = true, - OverdueDays = overdueDays, - CreatedAt = reader.GetDateTime(15), - UpdatedAt = reader.IsDBNull(16) ? null : reader.GetDateTime(16) - }); - } - - // 2.3 返回分页 - return new PagedResult(items, page, pageSize, total); - }, - cancellationToken); - } - - private static string BuildCountSql() - { - return """ - select count(*) - from public.tenant_billing_statements b - join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null - where b."DeletedAt" is null - and b."DueDate" < @now - and b."Status" in (0, 2); - """; - } - - private static string BuildListSql() - { - return """ - select - b."Id", - b."TenantId", - t."Name" as "TenantName", - b."SubscriptionId", - b."StatementNo", - b."BillingType", - b."PeriodStart", - b."PeriodEnd", - b."AmountDue", - b."DiscountAmount", - b."TaxAmount", - b."AmountPaid", - b."Status", - b."DueDate", - b."Currency", - b."CreatedAt", - b."UpdatedAt" - from public.tenant_billing_statements b - join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null - where b."DeletedAt" is null - and b."DueDate" < @now - and b."Status" in (0, 2) - order by b."DueDate" asc - 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/Billings/Handlers/GetTenantPaymentsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetTenantPaymentsQueryHandler.cs deleted file mode 100644 index 80d1538..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetTenantPaymentsQueryHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Application.App.Billings.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 获取租户支付记录查询处理器。 -/// -public sealed class GetTenantPaymentsQueryHandler(ITenantPaymentRepository paymentRepository) - : IRequestHandler> -{ - /// - /// 处理获取支付记录请求。 - /// - /// 查询请求。 - /// 取消标记。 - /// 支付记录列表。 - public async Task> Handle(GetTenantPaymentsQuery request, CancellationToken cancellationToken) - { - // 1. 查询支付记录 - var payments = await paymentRepository.GetByBillingIdAsync(request.BillId, cancellationToken); - - // 2. 映射并返回 DTO - return payments.Select(p => p.ToDto()).ToList(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ProcessOverdueBillingsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ProcessOverdueBillingsCommandHandler.cs deleted file mode 100644 index 0cb8c5b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/ProcessOverdueBillingsCommandHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Domain.Tenants.Services; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 处理逾期账单命令处理器(后台任务)。 -/// -public sealed class ProcessOverdueBillingsCommandHandler( - IBillingDomainService billingDomainService) - : IRequestHandler -{ - /// - public async Task Handle(ProcessOverdueBillingsCommand request, CancellationToken cancellationToken) - { - // 1. 委托领域服务执行逾期账单处理(Pending && DueDate < Now -> Overdue) - return await billingDomainService.ProcessOverdueBillingsAsync(cancellationToken); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/RecordPaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/RecordPaymentCommandHandler.cs deleted file mode 100644 index 47a81aa..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/RecordPaymentCommandHandler.cs +++ /dev/null @@ -1,81 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.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.Billings.Handlers; - -/// -/// 记录支付处理器。 -/// -public sealed class RecordPaymentCommandHandler( - ITenantBillingRepository billingRepository, - ITenantPaymentRepository paymentRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - /// 处理记录支付请求。 - /// - /// 记录支付命令。 - /// 取消标记。 - /// 支付 DTO。 - public async Task Handle(RecordPaymentCommand request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var billing = await billingRepository.FindByIdAsync(request.BillingId, cancellationToken); - if (billing is null) - { - throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); - } - - // 2. 业务规则检查 - if (billing.Status == TenantBillingStatus.Paid) - { - throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款"); - } - - if (billing.Status == TenantBillingStatus.Cancelled) - { - throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款"); - } - - // 3. 幂等校验:交易号唯一 - if (!string.IsNullOrWhiteSpace(request.TransactionNo)) - { - var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken); - if (exists is not null) - { - throw new BusinessException(ErrorCodes.Conflict, "交易号已存在,疑似重复提交"); - } - } - - // 4. 构建支付记录(默认待审核) - var now = DateTime.UtcNow; - var payment = new TenantPayment - { - Id = idGenerator.NextId(), - TenantId = billing.TenantId, - BillingStatementId = request.BillingId, - Amount = request.Amount, - Method = request.Method, - Status = TenantPaymentStatus.Pending, - TransactionNo = string.IsNullOrWhiteSpace(request.TransactionNo) ? null : request.TransactionNo.Trim(), - ProofUrl = request.ProofUrl, - PaidAt = now, - Notes = request.Notes - }; - - // 5. 持久化变更 - await paymentRepository.AddAsync(payment, cancellationToken); - await paymentRepository.SaveChangesAsync(cancellationToken); - - // 6. 返回 DTO - return payment.ToPaymentRecordDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillStatusCommandHandler.cs deleted file mode 100644 index 9a6f33b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillStatusCommandHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 更新账单状态处理器。 -/// -public sealed class UpdateBillStatusCommandHandler( - ITenantBillingRepository billingRepository, - ITenantRepository tenantRepository) - : IRequestHandler -{ - /// - /// 处理更新账单状态请求。 - /// - /// 更新命令。 - /// 取消标记。 - /// 账单 DTO 或 null。 - public async Task Handle(UpdateBillStatusCommand request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken); - if (bill is null) - { - return null; - } - - // 2. 更新状态 - bill.Status = request.Status; - if (!string.IsNullOrWhiteSpace(request.Notes)) - { - bill.LineItemsJson = request.Notes; - } - - // 3. 持久化变更 - await billingRepository.UpdateAsync(bill, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - // 4. 查询租户名称 - var tenant = await tenantRepository.FindByIdAsync(bill.TenantId, cancellationToken); - - // 5. 返回 DTO - return bill.ToDto(tenant?.Name); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillingStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillingStatusCommandHandler.cs deleted file mode 100644 index 049bdd1..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillingStatusCommandHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 更新账单状态命令处理器。 -/// -public sealed class UpdateBillingStatusCommandHandler( - ITenantBillingRepository billingRepository) - : IRequestHandler -{ - /// - public async Task Handle(UpdateBillingStatusCommand request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var billing = await billingRepository.FindByIdAsync(request.BillingId, cancellationToken); - if (billing is null) - { - throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); - } - - // 2. 状态转换规则校验 - if (billing.Status == TenantBillingStatus.Paid && request.NewStatus != TenantBillingStatus.Paid) - { - throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许改为其他状态"); - } - - if (billing.Status == TenantBillingStatus.Cancelled) - { - throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许变更状态"); - } - - // 3. 更新状态与备注 - billing.Status = request.NewStatus; - if (!string.IsNullOrWhiteSpace(request.Notes)) - { - billing.Notes = string.IsNullOrWhiteSpace(billing.Notes) - ? $"[状态变更] {request.Notes}" - : $"{billing.Notes}\n[状态变更] {request.Notes}"; - } - - // 4. 持久化 - await billingRepository.UpdateAsync(billing, cancellationToken); - await billingRepository.SaveChangesAsync(cancellationToken); - - return Unit.Value; - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/VerifyPaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/VerifyPaymentCommandHandler.cs deleted file mode 100644 index 0d58db4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/VerifyPaymentCommandHandler.cs +++ /dev/null @@ -1,73 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Application.App.Billings.Handlers; - -/// -/// 审核支付命令处理器。 -/// -public sealed class VerifyPaymentCommandHandler( - ITenantPaymentRepository paymentRepository, - ITenantBillingRepository billingRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - public async Task Handle(VerifyPaymentCommand request, CancellationToken cancellationToken) - { - // 1. 校验操作者身份 - if (!currentUserAccessor.IsAuthenticated || currentUserAccessor.UserId <= 0) - { - throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份"); - } - - // 2. 查询支付记录 - var payment = await paymentRepository.FindByIdAsync(request.PaymentId, cancellationToken); - if (payment is null) - { - throw new BusinessException(ErrorCodes.NotFound, "支付记录不存在"); - } - - // 3. 查询关联账单 - var billing = await billingRepository.FindByIdAsync(payment.BillingStatementId, cancellationToken); - if (billing is null) - { - throw new BusinessException(ErrorCodes.NotFound, "关联账单不存在"); - } - - // 4. 归一化审核备注 - var normalizedNotes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(); - - // 5. 根据审核结果更新支付与账单状态 - if (request.Approved) - { - payment.Verify(currentUserAccessor.UserId); - payment.Notes = normalizedNotes; - - billing.MarkAsPaid(payment.Amount, payment.TransactionNo ?? string.Empty); - } - else - { - payment.Reject(currentUserAccessor.UserId, normalizedNotes ?? string.Empty); - payment.Notes = normalizedNotes; - } - - // 6. 持久化更新状态 - await paymentRepository.UpdateAsync(payment, cancellationToken); - if (request.Approved) - { - await billingRepository.UpdateAsync(billing, cancellationToken); - } - - // 7. 保存数据库更改 - await paymentRepository.SaveChangesAsync(cancellationToken); - - // 8. 返回 DTO - return payment.ToPaymentRecordDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/ExportBillingsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/ExportBillingsQuery.cs deleted file mode 100644 index c800a21..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/ExportBillingsQuery.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 导出账单(Excel/PDF/CSV)。 -/// -public sealed record ExportBillingsQuery : IRequest -{ - /// - /// 要导出的账单 ID 列表。 - /// - public long[] BillingIds { get; init; } = []; - - /// - /// 导出格式(Excel/Pdf/Csv)。 - /// - public string Format { get; init; } = "Excel"; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillDetailQuery.cs deleted file mode 100644 index d155acd..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillDetailQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 获取账单详情查询。 -/// -public sealed record GetBillDetailQuery : IRequest -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillListQuery.cs deleted file mode 100644 index 903c333..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillListQuery.cs +++ /dev/null @@ -1,47 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 获取账单列表查询。 -/// -public sealed record GetBillListQuery : IRequest> -{ - /// - /// 页码(从 1 开始)。 - /// - public int PageNumber { get; init; } = 1; - - /// - /// 页大小。 - /// - public int PageSize { get; init; } = 20; - - /// - /// 租户 ID 筛选(可选)。 - /// - public long? TenantId { get; init; } - - /// - /// 状态筛选(可选)。 - /// - public TenantBillingStatus? Status { get; init; } - - /// - /// 开始日期筛选(可选)。 - /// - public DateTime? StartDate { get; init; } - - /// - /// 结束日期筛选(可选)。 - /// - public DateTime? EndDate { get; init; } - - /// - /// 搜索关键词(账单号或租户名)。 - /// - public string? Keyword { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingDetailQuery.cs deleted file mode 100644 index c4fc6fd..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingDetailQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 查询账单详情(含明细项)。 -/// -public sealed record GetBillingDetailQuery : IRequest -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillingId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs deleted file mode 100644 index da964e7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingListQuery.cs +++ /dev/null @@ -1,72 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 分页查询账单列表。 -/// -public sealed record GetBillingListQuery : IRequest> -{ - /// - /// 租户 ID(可选,管理员可查询所有租户)。 - /// - public long? TenantId { get; init; } - - /// - /// 账单状态筛选。 - /// - public TenantBillingStatus? Status { get; init; } - - /// - /// 账单类型筛选。 - /// - public BillingType? BillingType { get; init; } - - /// - /// 账单起始时间(UTC)筛选。 - /// - public DateTime? StartDate { get; init; } - - /// - /// 账单结束时间(UTC)筛选。 - /// - public DateTime? EndDate { get; init; } - - /// - /// 关键词搜索(账单编号)。 - /// - public string? Keyword { get; init; } - - /// - /// 最小应付金额筛选(包含)。 - /// - public decimal? MinAmount { get; init; } - - /// - /// 最大应付金额筛选(包含)。 - /// - public decimal? MaxAmount { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int PageNumber { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; - - /// - /// 排序字段(DueDate/CreatedAt/AmountDue)。 - /// - public string? SortBy { get; init; } - - /// - /// 是否降序排序。 - /// - public bool SortDesc { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingPaymentsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingPaymentsQuery.cs deleted file mode 100644 index 04e01c4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingPaymentsQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 查询账单的支付记录。 -/// -public sealed record GetBillingPaymentsQuery : IRequest> -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillingId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs deleted file mode 100644 index 5927f57..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillingStatisticsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 查询账单统计数据。 -/// -public sealed record GetBillingStatisticsQuery : IRequest -{ - /// - /// 租户 ID(可选,管理员可查询所有租户)。 - /// - public long? TenantId { get; init; } - - /// - /// 统计开始时间(UTC)。 - /// - public DateTime? StartDate { get; init; } - - /// - /// 统计结束时间(UTC)。 - /// - public DateTime? EndDate { get; init; } - - /// - /// 分组方式(Day/Week/Month)。 - /// - public string? GroupBy { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetOverdueBillingsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetOverdueBillingsQuery.cs deleted file mode 100644 index 171019e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetOverdueBillingsQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 查询逾期账单列表。 -/// -public sealed record GetOverdueBillingsQuery : IRequest> -{ - /// - /// 页码(从 1 开始)。 - /// - public int PageNumber { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetTenantPaymentsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetTenantPaymentsQuery.cs deleted file mode 100644 index 986ca41..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetTenantPaymentsQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Billings.Dto; - -namespace TakeoutSaaS.Application.App.Billings.Queries; - -/// -/// 获取租户支付记录查询。 -/// -public sealed record GetTenantPaymentsQuery : IRequest> -{ - /// - /// 账单 ID(雪花算法)。 - /// - public long BillId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/ConfirmPaymentCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Validators/ConfirmPaymentCommandValidator.cs deleted file mode 100644 index d1d8f75..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/ConfirmPaymentCommandValidator.cs +++ /dev/null @@ -1,50 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Billings.Commands; - -namespace TakeoutSaaS.Application.App.Billings.Validators; - -/// -/// 一键确认收款命令验证器。 -/// -public sealed class ConfirmPaymentCommandValidator : AbstractValidator -{ - public ConfirmPaymentCommandValidator() - { - // 1. 账单 ID 必填 - RuleFor(x => x.BillingId) - .GreaterThan(0) - .WithMessage("账单 ID 必须大于 0"); - - // 2. 支付金额必须大于 0 - RuleFor(x => x.Amount) - .GreaterThan(0) - .WithMessage("支付金额必须大于 0") - .LessThanOrEqualTo(1_000_000_000) - .WithMessage("支付金额不能超过 10 亿"); - - // 3. 支付方式必填 - RuleFor(x => x.Method) - .IsInEnum() - .WithMessage("支付方式无效"); - - // 4. 交易号必填 - RuleFor(x => x.TransactionNo) - .NotEmpty() - .WithMessage("交易号不能为空") - .MaximumLength(64) - .WithMessage("交易号不能超过 64 个字符"); - - // 5. 支付凭证 URL(可选) - RuleFor(x => x.ProofUrl) - .MaximumLength(500) - .WithMessage("支付凭证 URL 不能超过 500 个字符") - .When(x => !string.IsNullOrWhiteSpace(x.ProofUrl)); - - // 6. 备注(可选) - RuleFor(x => x.Notes) - .MaximumLength(500) - .WithMessage("备注不能超过 500 个字符") - .When(x => !string.IsNullOrWhiteSpace(x.Notes)); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/CreateBillingCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Validators/CreateBillingCommandValidator.cs deleted file mode 100644 index 14483f7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/CreateBillingCommandValidator.cs +++ /dev/null @@ -1,73 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Billings.Commands; - -namespace TakeoutSaaS.Application.App.Billings.Validators; - -/// -/// 创建账单命令验证器。 -/// -public sealed class CreateBillingCommandValidator : AbstractValidator -{ - public CreateBillingCommandValidator() - { - // 1. 租户 ID 必填 - RuleFor(x => x.TenantId) - .GreaterThan(0) - .WithMessage("租户 ID 必须大于 0"); - - // 2. 账单类型必填 - RuleFor(x => x.BillingType) - .IsInEnum() - .WithMessage("账单类型无效"); - - // 3. 应付金额必须大于 0 - RuleFor(x => x.AmountDue) - .GreaterThan(0) - .WithMessage("应付金额必须大于 0"); - - // 4. 到期日必须是未来时间 - RuleFor(x => x.DueDate) - .GreaterThan(DateTime.UtcNow) - .WithMessage("到期日必须是未来时间"); - - // 5. 账单明细至少包含一项 - RuleFor(x => x.LineItems) - .NotEmpty() - .WithMessage("账单明细不能为空"); - - // 6. 账单明细项验证 - RuleForEach(x => x.LineItems) - .ChildRules(lineItem => - { - lineItem.RuleFor(x => x.ItemType) - .NotEmpty() - .WithMessage("账单明细类型不能为空") - .MaximumLength(50) - .WithMessage("账单明细类型不能超过 50 个字符"); - - lineItem.RuleFor(x => x.Description) - .NotEmpty() - .WithMessage("账单明细描述不能为空") - .MaximumLength(200) - .WithMessage("账单明细描述不能超过 200 个字符"); - - lineItem.RuleFor(x => x.Quantity) - .GreaterThan(0) - .WithMessage("账单明细数量必须大于 0"); - - lineItem.RuleFor(x => x.UnitPrice) - .GreaterThanOrEqualTo(0) - .WithMessage("账单明细单价不能为负数"); - - lineItem.RuleFor(x => x.Amount) - .GreaterThanOrEqualTo(0) - .WithMessage("账单明细金额不能为负数"); - }); - - // 7. 备注长度限制(可选) - RuleFor(x => x.Notes) - .MaximumLength(500) - .WithMessage("备注不能超过 500 个字符") - .When(x => !string.IsNullOrWhiteSpace(x.Notes)); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/RecordPaymentCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Validators/RecordPaymentCommandValidator.cs deleted file mode 100644 index 266eb1e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/RecordPaymentCommandValidator.cs +++ /dev/null @@ -1,49 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Billings.Commands; - -namespace TakeoutSaaS.Application.App.Billings.Validators; - -/// -/// 记录支付命令验证器。 -/// -public sealed class RecordPaymentCommandValidator : AbstractValidator -{ - public RecordPaymentCommandValidator() - { - // 1. 账单 ID 必填 - RuleFor(x => x.BillingId) - .GreaterThan(0) - .WithMessage("账单 ID 必须大于 0"); - - // 2. 支付金额必须大于 0 - RuleFor(x => x.Amount) - .GreaterThan(0) - .WithMessage("支付金额必须大于 0") - .LessThanOrEqualTo(1_000_000_000) - .WithMessage("支付金额不能超过 10 亿"); - - // 3. 支付方式必填 - RuleFor(x => x.Method) - .IsInEnum() - .WithMessage("支付方式无效"); - - // 4. 交易号必填 - RuleFor(x => x.TransactionNo) - .NotEmpty() - .WithMessage("交易号不能为空") - .MaximumLength(64) - .WithMessage("交易号不能超过 64 个字符"); - - // 5. 支付凭证 URL(可选) - RuleFor(x => x.ProofUrl) - .MaximumLength(500) - .WithMessage("支付凭证 URL 不能超过 500 个字符") - .When(x => !string.IsNullOrWhiteSpace(x.ProofUrl)); - - // 6. 备注(可选) - RuleFor(x => x.Notes) - .MaximumLength(500) - .WithMessage("备注不能超过 500 个字符") - .When(x => !string.IsNullOrWhiteSpace(x.Notes)); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/UpdateBillingStatusCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Validators/UpdateBillingStatusCommandValidator.cs deleted file mode 100644 index 025b6ed..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Validators/UpdateBillingStatusCommandValidator.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Billings.Commands; - -namespace TakeoutSaaS.Application.App.Billings.Validators; - -/// -/// 更新账单状态命令验证器。 -/// -public sealed class UpdateBillingStatusCommandValidator : AbstractValidator -{ - public UpdateBillingStatusCommandValidator() - { - // 1. 账单 ID 必填 - RuleFor(x => x.BillingId) - .GreaterThan(0) - .WithMessage("账单 ID 必须大于 0"); - - // 2. 状态枚举校验 - RuleFor(x => x.NewStatus) - .IsInEnum() - .WithMessage("新状态无效"); - - // 3. 备注长度限制(可选) - RuleFor(x => x.Notes) - .MaximumLength(500) - .WithMessage("备注不能超过 500 个字符") - .When(x => !string.IsNullOrWhiteSpace(x.Notes)); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/CreateQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/CreateQuotaPackageCommand.cs deleted file mode 100644 index 7c8de9e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/CreateQuotaPackageCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; - -/// -/// 创建配额包命令。 -/// -public sealed record CreateQuotaPackageCommand : IRequest -{ - /// - /// 配额包名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 配额数值。 - /// - public decimal QuotaValue { get; init; } - - /// - /// 价格。 - /// - public decimal Price { get; init; } - - /// - /// 是否上架。 - /// - public bool IsActive { get; init; } = true; - - /// - /// 排序。 - /// - public int SortOrder { get; init; } = 0; - - /// - /// 描述。 - /// - public string? Description { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/DeleteQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/DeleteQuotaPackageCommand.cs deleted file mode 100644 index 62c8f3f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/DeleteQuotaPackageCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; - -/// -/// 删除配额包命令。 -/// -public sealed record DeleteQuotaPackageCommand : IRequest -{ - /// - /// 配额包 ID。 - /// - public long QuotaPackageId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/PurchaseQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/PurchaseQuotaPackageCommand.cs deleted file mode 100644 index 3485036..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/PurchaseQuotaPackageCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; - -/// -/// 为租户购买配额包命令。 -/// -public sealed record PurchaseQuotaPackageCommand : IRequest -{ - /// - /// 租户 ID。 - /// - public long TenantId { get; init; } - - /// - /// 配额包 ID。 - /// - public long QuotaPackageId { get; init; } - - /// - /// 过期时间(可选)。 - /// - public DateTime? ExpiredAt { get; init; } - - /// - /// 备注。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageCommand.cs deleted file mode 100644 index f6f43c3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageCommand.cs +++ /dev/null @@ -1,51 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; - -/// -/// 更新配额包命令。 -/// -public sealed record UpdateQuotaPackageCommand : IRequest -{ - /// - /// 配额包 ID。 - /// - public long QuotaPackageId { get; init; } - - /// - /// 配额包名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 配额数值。 - /// - public decimal QuotaValue { get; init; } - - /// - /// 价格。 - /// - public decimal Price { get; init; } - - /// - /// 是否上架。 - /// - public bool IsActive { get; init; } = true; - - /// - /// 排序。 - /// - public int SortOrder { get; init; } = 0; - - /// - /// 描述。 - /// - public string? Description { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageStatusCommand.cs deleted file mode 100644 index 4a0fdb0..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageStatusCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; - -/// -/// 更新配额包状态命令(上架/下架)。 -/// -public sealed record UpdateQuotaPackageStatusCommand : IRequest -{ - /// - /// 配额包 ID。 - /// - public long QuotaPackageId { get; init; } - - /// - /// 是否上架。 - /// - public bool IsActive { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageDto.cs deleted file mode 100644 index f710e8c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageDto.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; - -/// -/// 配额包 DTO。 -/// -public sealed record QuotaPackageDto -{ - /// - /// 配额包 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 配额包名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 配额数值。 - /// - public decimal QuotaValue { get; init; } - - /// - /// 价格。 - /// - public decimal Price { get; init; } - - /// - /// 是否上架。 - /// - public bool IsActive { get; init; } - - /// - /// 排序。 - /// - public int SortOrder { get; init; } - - /// - /// 描述。 - /// - public string? Description { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 更新时间。 - /// - public DateTime? UpdatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageListDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageListDto.cs deleted file mode 100644 index 9dd8fdf..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageListDto.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; - -/// -/// 配额包列表 DTO。 -/// -public sealed record QuotaPackageListDto -{ - /// - /// 配额包 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 配额包名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 配额数值。 - /// - public decimal QuotaValue { get; init; } - - /// - /// 价格。 - /// - public decimal Price { get; init; } - - /// - /// 是否上架。 - /// - public bool IsActive { get; init; } - - /// - /// 排序。 - /// - public int SortOrder { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaPurchaseDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaPurchaseDto.cs deleted file mode 100644 index 2ce1bb4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaPurchaseDto.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; - -/// -/// 租户配额购买记录 DTO。 -/// -public sealed record TenantQuotaPurchaseDto -{ - /// - /// 购买记录 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 配额包 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long QuotaPackageId { get; init; } - - /// - /// 配额包名称。 - /// - public string QuotaPackageName { get; init; } = string.Empty; - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 购买时的配额值。 - /// - public decimal QuotaValue { get; init; } - - /// - /// 购买价格。 - /// - public decimal Price { get; init; } - - /// - /// 购买时间。 - /// - public DateTime PurchasedAt { get; init; } - - /// - /// 过期时间(可选)。 - /// - public DateTime? ExpiredAt { get; init; } - - /// - /// 备注。 - /// - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaUsageDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaUsageDto.cs deleted file mode 100644 index a651fd8..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaUsageDto.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; - -/// -/// 租户配额使用情况 DTO。 -/// -public sealed record TenantQuotaUsageDto -{ - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 配额上限。 - /// - public decimal LimitValue { get; init; } - - /// - /// 已使用值。 - /// - public decimal UsedValue { get; init; } - - /// - /// 剩余值。 - /// - public decimal RemainingValue { get; init; } - - /// - /// 配额刷新周期。 - /// - public string? ResetCycle { get; init; } - - /// - /// 最近一次重置时间。 - /// - public DateTime? LastResetAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/CreateQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/CreateQuotaPackageCommandHandler.cs deleted file mode 100644 index 28c9b3a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/CreateQuotaPackageCommandHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Commands; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 创建配额包命令处理器。 -/// -public sealed class CreateQuotaPackageCommandHandler( - IQuotaPackageRepository quotaPackageRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(CreateQuotaPackageCommand request, CancellationToken cancellationToken) - { - // 1. 创建配额包实体 - var quotaPackage = new QuotaPackage - { - Id = idGenerator.NextId(), - Name = request.Name, - QuotaType = request.QuotaType, - QuotaValue = request.QuotaValue, - Price = request.Price, - IsActive = request.IsActive, - SortOrder = request.SortOrder, - Description = request.Description, - CreatedAt = DateTime.UtcNow - }; - - // 2. 保存到数据库 - await quotaPackageRepository.AddAsync(quotaPackage, cancellationToken); - await quotaPackageRepository.SaveChangesAsync(cancellationToken); - - // 3. 返回 DTO - return new QuotaPackageDto - { - Id = quotaPackage.Id, - Name = quotaPackage.Name, - QuotaType = quotaPackage.QuotaType, - QuotaValue = quotaPackage.QuotaValue, - Price = quotaPackage.Price, - IsActive = quotaPackage.IsActive, - SortOrder = quotaPackage.SortOrder, - Description = quotaPackage.Description, - CreatedAt = quotaPackage.CreatedAt, - UpdatedAt = quotaPackage.UpdatedAt - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/DeleteQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/DeleteQuotaPackageCommandHandler.cs deleted file mode 100644 index 1180d7e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/DeleteQuotaPackageCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Commands; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 删除配额包命令处理器。 -/// -public sealed class DeleteQuotaPackageCommandHandler(IQuotaPackageRepository quotaPackageRepository) - : IRequestHandler -{ - /// - public async Task Handle(DeleteQuotaPackageCommand request, CancellationToken cancellationToken) - { - // 1. 软删除配额包 - var deleted = await quotaPackageRepository.SoftDeleteAsync(request.QuotaPackageId, cancellationToken); - - if (!deleted) - { - return false; - } - - // 2. 保存变更 - await quotaPackageRepository.SaveChangesAsync(cancellationToken); - - return true; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetQuotaPackageListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetQuotaPackageListQueryHandler.cs deleted file mode 100644 index a61d883..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetQuotaPackageListQueryHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Application.App.QuotaPackages.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 获取配额包列表查询处理器。 -/// -public sealed class GetQuotaPackageListQueryHandler(IQuotaPackageRepository quotaPackageRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(GetQuotaPackageListQuery request, CancellationToken cancellationToken) - { - // 1. 分页查询 - var (items, total) = await quotaPackageRepository.SearchPagedAsync( - request.QuotaType, - request.IsActive, - request.Page, - request.PageSize, - cancellationToken); - - // 2. 映射为 DTO - var dtos = items.Select(x => new QuotaPackageListDto - { - Id = x.Id, - Name = x.Name, - QuotaType = x.QuotaType, - QuotaValue = x.QuotaValue, - Price = x.Price, - IsActive = x.IsActive, - SortOrder = x.SortOrder - }).ToList(); - - // 3. 返回分页结果 - return new PagedResult(dtos, request.Page, request.PageSize, total); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaPurchasesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaPurchasesQueryHandler.cs deleted file mode 100644 index 1b232d3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaPurchasesQueryHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Application.App.QuotaPackages.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 获取租户配额购买记录查询处理器。 -/// -public sealed class GetTenantQuotaPurchasesQueryHandler(IQuotaPackageRepository quotaPackageRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(GetTenantQuotaPurchasesQuery request, CancellationToken cancellationToken) - { - // 1. 分页查询购买记录 - var (items, total) = await quotaPackageRepository.GetPurchasesPagedAsync( - request.TenantId, - request.Page, - request.PageSize, - cancellationToken); - - // 2. 映射为 DTO - var dtos = items.Select(x => new TenantQuotaPurchaseDto - { - Id = x.Purchase.Id, - TenantId = x.Purchase.TenantId, - QuotaPackageId = x.Purchase.QuotaPackageId, - QuotaPackageName = x.Package.Name, - QuotaType = x.Package.QuotaType, - QuotaValue = x.Purchase.QuotaValue, - Price = x.Purchase.Price, - PurchasedAt = x.Purchase.PurchasedAt, - ExpiredAt = x.Purchase.ExpiredAt, - Notes = x.Purchase.Notes - }).ToList(); - - // 3. 返回分页结果 - return new PagedResult(dtos, request.Page, request.PageSize, total); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaUsageQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaUsageQueryHandler.cs deleted file mode 100644 index ee246b6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaUsageQueryHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Application.App.QuotaPackages.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 获取租户配额使用情况查询处理器。 -/// -public sealed class GetTenantQuotaUsageQueryHandler(IQuotaPackageRepository quotaPackageRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(GetTenantQuotaUsageQuery request, CancellationToken cancellationToken) - { - // 1. 查询配额使用情况 - var items = await quotaPackageRepository.GetUsageByTenantAsync( - request.TenantId, - request.QuotaType, - cancellationToken); - - // 2. 映射为 DTO - return items.Select(x => new TenantQuotaUsageDto - { - TenantId = x.TenantId, - QuotaType = x.QuotaType, - LimitValue = x.LimitValue, - UsedValue = x.UsedValue, - RemainingValue = x.LimitValue - x.UsedValue, - ResetCycle = x.ResetCycle, - LastResetAt = x.LastResetAt - }).ToList(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/PurchaseQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/PurchaseQuotaPackageCommandHandler.cs deleted file mode 100644 index 65d7a68..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/PurchaseQuotaPackageCommandHandler.cs +++ /dev/null @@ -1,88 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Commands; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 购买配额包命令处理器。 -/// -public sealed class PurchaseQuotaPackageCommandHandler( - IQuotaPackageRepository quotaPackageRepository, - ITenantQuotaUsageHistoryRepository quotaUsageHistoryRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(PurchaseQuotaPackageCommand request, CancellationToken cancellationToken) - { - // 1. 查找配额包 - var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken); - - if (quotaPackage == null) - { - throw new InvalidOperationException("配额包不存在"); - } - - // 2. 创建购买记录 - var purchase = new TenantQuotaPackagePurchase - { - Id = idGenerator.NextId(), - TenantId = request.TenantId, - QuotaPackageId = request.QuotaPackageId, - QuotaValue = quotaPackage.QuotaValue, - Price = quotaPackage.Price, - PurchasedAt = DateTime.UtcNow, - ExpiredAt = request.ExpiredAt, - Notes = request.Notes, - CreatedAt = DateTime.UtcNow - }; - - // 3. 保存购买记录 - await quotaPackageRepository.AddPurchaseAsync(purchase, cancellationToken); - - // 4. 更新租户配额(根据配额类型更新对应配额) - var quotaUsage = await quotaPackageRepository.FindUsageAsync(request.TenantId, quotaPackage.QuotaType, cancellationToken); - - if (quotaUsage != null) - { - var beforeLimit = quotaUsage.LimitValue; - quotaUsage.LimitValue += quotaPackage.QuotaValue; - await quotaPackageRepository.UpdateUsageAsync(quotaUsage, cancellationToken); - - // 4.1 记录配额变更历史(购买配额包视为“剩余增加”) - await quotaUsageHistoryRepository.AddAsync(new TenantQuotaUsageHistory - { - TenantId = request.TenantId, - QuotaType = quotaPackage.QuotaType, - UsedValue = quotaUsage.UsedValue, - LimitValue = quotaUsage.LimitValue, - RecordedAt = DateTime.UtcNow, - ChangeType = TenantQuotaUsageHistoryChangeType.Increase, - ChangeAmount = quotaUsage.LimitValue - beforeLimit, - ChangeReason = $"购买配额包:{quotaPackage.Name}" - }, cancellationToken); - } - - await quotaPackageRepository.SaveChangesAsync(cancellationToken); - - // 5. 返回 DTO - return new TenantQuotaPurchaseDto - { - Id = purchase.Id, - TenantId = purchase.TenantId, - QuotaPackageId = purchase.QuotaPackageId, - QuotaPackageName = quotaPackage.Name, - QuotaType = quotaPackage.QuotaType, - QuotaValue = purchase.QuotaValue, - Price = purchase.Price, - PurchasedAt = purchase.PurchasedAt, - ExpiredAt = purchase.ExpiredAt, - Notes = purchase.Notes - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageCommandHandler.cs deleted file mode 100644 index 5e57207..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageCommandHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Commands; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 更新配额包命令处理器。 -/// -public sealed class UpdateQuotaPackageCommandHandler(IQuotaPackageRepository quotaPackageRepository) - : IRequestHandler -{ - /// - public async Task Handle(UpdateQuotaPackageCommand request, CancellationToken cancellationToken) - { - // 1. 查找配额包 - var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken); - - if (quotaPackage == null) - { - return null; - } - - // 2. 更新配额包 - quotaPackage.Name = request.Name; - quotaPackage.QuotaType = request.QuotaType; - quotaPackage.QuotaValue = request.QuotaValue; - quotaPackage.Price = request.Price; - quotaPackage.IsActive = request.IsActive; - quotaPackage.SortOrder = request.SortOrder; - quotaPackage.Description = request.Description; - quotaPackage.UpdatedAt = DateTime.UtcNow; - - // 3. 保存到数据库 - await quotaPackageRepository.UpdateAsync(quotaPackage, cancellationToken); - await quotaPackageRepository.SaveChangesAsync(cancellationToken); - - // 4. 返回 DTO - return new QuotaPackageDto - { - Id = quotaPackage.Id, - Name = quotaPackage.Name, - QuotaType = quotaPackage.QuotaType, - QuotaValue = quotaPackage.QuotaValue, - Price = quotaPackage.Price, - IsActive = quotaPackage.IsActive, - SortOrder = quotaPackage.SortOrder, - Description = quotaPackage.Description, - CreatedAt = quotaPackage.CreatedAt, - UpdatedAt = quotaPackage.UpdatedAt - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageStatusCommandHandler.cs deleted file mode 100644 index e5ea40b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageStatusCommandHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Commands; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; - -/// -/// 更新配额包状态命令处理器。 -/// -public sealed class UpdateQuotaPackageStatusCommandHandler(IQuotaPackageRepository quotaPackageRepository) - : IRequestHandler -{ - /// - public async Task Handle(UpdateQuotaPackageStatusCommand request, CancellationToken cancellationToken) - { - // 1. 查找配额包 - var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken); - - if (quotaPackage == null) - { - return false; - } - - // 2. 更新状态 - quotaPackage.IsActive = request.IsActive; - quotaPackage.UpdatedAt = DateTime.UtcNow; - - // 3. 保存到数据库 - await quotaPackageRepository.UpdateAsync(quotaPackage, cancellationToken); - await quotaPackageRepository.SaveChangesAsync(cancellationToken); - - return true; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetQuotaPackageListQuery.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetQuotaPackageListQuery.cs deleted file mode 100644 index 538d1ef..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetQuotaPackageListQuery.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Queries; - -/// -/// 获取配额包列表查询。 -/// -public sealed record GetQuotaPackageListQuery : IRequest> -{ - /// - /// 配额类型(可选筛选)。 - /// - public TenantQuotaType? QuotaType { 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/QuotaPackages/Queries/GetTenantQuotaPurchasesQuery.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaPurchasesQuery.cs deleted file mode 100644 index 56348e6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaPurchasesQuery.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Queries; - -/// -/// 获取租户配额购买记录查询。 -/// -public sealed record GetTenantQuotaPurchasesQuery : IRequest> -{ - /// - /// 租户 ID。 - /// - 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/QuotaPackages/Queries/GetTenantQuotaUsageQuery.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaUsageQuery.cs deleted file mode 100644 index f5b395c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaUsageQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.QuotaPackages.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.QuotaPackages.Queries; - -/// -/// 获取租户配额使用情况查询。 -/// -public sealed record GetTenantQuotaUsageQuery : IRequest> -{ - /// - /// 租户 ID。 - /// - public long TenantId { get; init; } - - /// - /// 配额类型(可选筛选)。 - /// - public TenantQuotaType? QuotaType { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/ExpiringSubscriptionDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/ExpiringSubscriptionDto.cs deleted file mode 100644 index c9a6d30..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/ExpiringSubscriptionDto.cs +++ /dev/null @@ -1,49 +0,0 @@ -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Statistics.Dto; - -/// -/// 即将到期的订阅项。 -/// -public record ExpiringSubscriptionDto -{ - /// - /// 订阅ID。 - /// - public long Id { get; init; } - - /// - /// 租户ID。 - /// - public string TenantId { get; init; } = string.Empty; - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 套餐名称。 - /// - public string PackageName { get; init; } = string.Empty; - - /// - /// 订阅状态。 - /// - public SubscriptionStatus Status { get; init; } - - /// - /// 到期时间。 - /// - public DateTime EffectiveTo { get; init; } - - /// - /// 剩余天数。 - /// - public int DaysRemaining { get; init; } - - /// - /// 是否开启自动续费。 - /// - public bool AutoRenew { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/QuotaUsageRankingDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/QuotaUsageRankingDto.cs deleted file mode 100644 index 57f53c2..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/QuotaUsageRankingDto.cs +++ /dev/null @@ -1,50 +0,0 @@ -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Statistics.Dto; - -/// -/// 配额使用排行。 -/// -public record QuotaUsageRankingDto -{ - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 排行列表。 - /// - public IReadOnlyList Rankings { get; init; } = Array.Empty(); -} - -/// -/// 配额使用排行项。 -/// -public record QuotaUsageRankItem -{ - /// - /// 租户ID。 - /// - public string TenantId { get; init; } = string.Empty; - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 已使用值。 - /// - public decimal UsedValue { get; init; } - - /// - /// 限制值。 - /// - public decimal LimitValue { get; init; } - - /// - /// 使用百分比。 - /// - public decimal UsagePercentage { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/RevenueStatisticsDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/RevenueStatisticsDto.cs deleted file mode 100644 index ceb604f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/RevenueStatisticsDto.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace TakeoutSaaS.Application.App.Statistics.Dto; - -/// -/// 收入统计。 -/// -public record RevenueStatisticsDto -{ - /// - /// 总收入。 - /// - public decimal TotalRevenue { get; init; } - - /// - /// 本月收入。 - /// - public decimal MonthlyRevenue { get; init; } - - /// - /// 本季度收入。 - /// - public decimal QuarterlyRevenue { get; init; } - - /// - /// 月度收入明细。 - /// - public IReadOnlyList MonthlyDetails { get; init; } = Array.Empty(); -} - -/// -/// 月度收入项。 -/// -public record MonthlyRevenueItem -{ - /// - /// 年份。 - /// - public int Year { get; init; } - - /// - /// 月份。 - /// - public int Month { get; init; } - - /// - /// 收入金额。 - /// - public decimal Amount { get; init; } - - /// - /// 账单数量。 - /// - public int BillCount { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/SubscriptionOverviewDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/SubscriptionOverviewDto.cs deleted file mode 100644 index 9323faa..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/SubscriptionOverviewDto.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace TakeoutSaaS.Application.App.Statistics.Dto; - -/// -/// 订阅概览。 -/// -public record SubscriptionOverviewDto -{ - /// - /// 活跃订阅总数。 - /// - public int TotalActive { get; init; } - - /// - /// 7天内到期数量。 - /// - public int ExpiringIn7Days { get; init; } - - /// - /// 3天内到期数量。 - /// - public int ExpiringIn3Days { get; init; } - - /// - /// 1天内到期数量。 - /// - public int ExpiringIn1Day { get; init; } - - /// - /// 已过期数量。 - /// - public int Expired { get; init; } - - /// - /// 待激活数量。 - /// - public int Pending { get; init; } - - /// - /// 已暂停数量。 - /// - public int Suspended { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetExpiringSubscriptionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetExpiringSubscriptionsQueryHandler.cs deleted file mode 100644 index a913aac..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetExpiringSubscriptionsQueryHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; -using TakeoutSaaS.Application.App.Statistics.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Statistics.Handlers; - -/// -/// 获取即将到期的订阅列表处理器。 -/// -public sealed class GetExpiringSubscriptionsQueryHandler(IStatisticsRepository statisticsRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(GetExpiringSubscriptionsQuery request, CancellationToken cancellationToken) - { - var now = DateTime.UtcNow; - - // 查询即将到期的订阅 - var items = await statisticsRepository.GetExpiringSubscriptionsAsync( - request.DaysAhead, - request.OnlyWithoutAutoRenew, - cancellationToken); - - // 映射为 DTO - return items.Select(x => new ExpiringSubscriptionDto - { - Id = x.Subscription.Id, - TenantId = x.Subscription.TenantId.ToString(), - TenantName = x.TenantName, - PackageName = x.PackageName, - Status = x.Subscription.Status, - EffectiveTo = x.Subscription.EffectiveTo, - DaysRemaining = (int)(x.Subscription.EffectiveTo - now).TotalDays, - AutoRenew = x.Subscription.AutoRenew - }).ToList(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetQuotaUsageRankingQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetQuotaUsageRankingQueryHandler.cs deleted file mode 100644 index b6fc116..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetQuotaUsageRankingQueryHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; -using TakeoutSaaS.Application.App.Statistics.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Statistics.Handlers; - -/// -/// 获取配额使用排行处理器。 -/// -public sealed class GetQuotaUsageRankingQueryHandler(IStatisticsRepository statisticsRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetQuotaUsageRankingQuery request, CancellationToken cancellationToken) - { - // 查询指定类型的配额使用排行 - var items = await statisticsRepository.GetQuotaUsageRankingAsync( - request.QuotaType, - request.TopN, - cancellationToken); - - // 映射为 DTO - var rankings = items.Select(x => new QuotaUsageRankItem - { - TenantId = x.TenantId.ToString(), - TenantName = x.TenantName, - UsedValue = x.UsedValue, - LimitValue = x.LimitValue, - UsagePercentage = x.UsagePercentage - }).ToList(); - - return new QuotaUsageRankingDto - { - QuotaType = request.QuotaType, - Rankings = rankings - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetRevenueStatisticsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetRevenueStatisticsQueryHandler.cs deleted file mode 100644 index 55cee9f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetRevenueStatisticsQueryHandler.cs +++ /dev/null @@ -1,71 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; -using TakeoutSaaS.Application.App.Statistics.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Statistics.Handlers; - -/// -/// 获取收入统计处理器。 -/// -public sealed class GetRevenueStatisticsQueryHandler(IStatisticsRepository statisticsRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetRevenueStatisticsQuery request, CancellationToken cancellationToken) - { - var now = DateTime.UtcNow; - var currentMonth = new DateTime(now.Year, now.Month, 1); - var currentQuarter = GetQuarterStart(now); - var startMonth = currentMonth.AddMonths(-request.MonthsCount + 1); - - // 查询所有已付款的账单 - var bills = await statisticsRepository.GetPaidBillsAsync(cancellationToken); - - // 总收入 - var totalRevenue = bills.Sum(b => b.AmountPaid); - - // 本月收入 - var monthlyRevenue = bills - .Where(b => b.PeriodStart >= currentMonth) - .Sum(b => b.AmountPaid); - - // 本季度收入 - var quarterlyRevenue = bills - .Where(b => b.PeriodStart >= currentQuarter) - .Sum(b => b.AmountPaid); - - // 月度收入明细 - var monthlyDetails = bills - .Where(b => b.PeriodStart >= startMonth) - .GroupBy(b => new { b.PeriodStart.Year, b.PeriodStart.Month }) - .Select(g => new MonthlyRevenueItem - { - Year = g.Key.Year, - Month = g.Key.Month, - Amount = g.Sum(b => b.AmountPaid), - BillCount = g.Count() - }) - .OrderBy(m => m.Year) - .ThenBy(m => m.Month) - .ToList(); - - return new RevenueStatisticsDto - { - TotalRevenue = totalRevenue, - MonthlyRevenue = monthlyRevenue, - QuarterlyRevenue = quarterlyRevenue, - MonthlyDetails = monthlyDetails - }; - } - - /// - /// 获取季度开始时间。 - /// - private static DateTime GetQuarterStart(DateTime date) - { - var quarter = (date.Month - 1) / 3; - var quarterStartMonth = quarter * 3 + 1; - return new DateTime(date.Year, quarterStartMonth, 1); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetSubscriptionOverviewQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetSubscriptionOverviewQueryHandler.cs deleted file mode 100644 index 054044b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetSubscriptionOverviewQueryHandler.cs +++ /dev/null @@ -1,49 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; -using TakeoutSaaS.Application.App.Statistics.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Statistics.Handlers; - -/// -/// 获取订阅概览统计处理器。 -/// -public sealed class GetSubscriptionOverviewQueryHandler(IStatisticsRepository statisticsRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetSubscriptionOverviewQuery request, CancellationToken cancellationToken) - { - var now = DateTime.UtcNow; - var in7Days = now.AddDays(7); - var in3Days = now.AddDays(3); - var in1Day = now.AddDays(1); - - // 查询所有订阅 - var subscriptions = await statisticsRepository.GetAllSubscriptionsAsync(cancellationToken); - - // 统计各项数据 - var overview = new SubscriptionOverviewDto - { - TotalActive = subscriptions.Count(s => s.Status == SubscriptionStatus.Active), - ExpiringIn7Days = subscriptions.Count(s => - s.Status == SubscriptionStatus.Active && - s.EffectiveTo >= now && - s.EffectiveTo <= in7Days), - ExpiringIn3Days = subscriptions.Count(s => - s.Status == SubscriptionStatus.Active && - s.EffectiveTo >= now && - s.EffectiveTo <= in3Days), - ExpiringIn1Day = subscriptions.Count(s => - s.Status == SubscriptionStatus.Active && - s.EffectiveTo >= now && - s.EffectiveTo <= in1Day), - Expired = subscriptions.Count(s => s.Status == SubscriptionStatus.GracePeriod), - Pending = subscriptions.Count(s => s.Status == SubscriptionStatus.Pending), - Suspended = subscriptions.Count(s => s.Status == SubscriptionStatus.Suspended) - }; - - return overview; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetExpiringSubscriptionsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetExpiringSubscriptionsQuery.cs deleted file mode 100644 index 3fcd2e6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetExpiringSubscriptionsQuery.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; - -namespace TakeoutSaaS.Application.App.Statistics.Queries; - -/// -/// 获取即将到期的订阅列表。 -/// -public sealed record GetExpiringSubscriptionsQuery : IRequest> -{ - /// - /// 筛选天数,默认7天内到期。 - /// - public int DaysAhead { get; init; } = 7; - - /// - /// 是否只返回未开启自动续费的订阅。 - /// - public bool OnlyWithoutAutoRenew { get; init; } = false; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetQuotaUsageRankingQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetQuotaUsageRankingQuery.cs deleted file mode 100644 index af8d0cc..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetQuotaUsageRankingQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Statistics.Queries; - -/// -/// 获取配额使用排行。 -/// -public sealed record GetQuotaUsageRankingQuery : IRequest -{ - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 返回前N条记录,默认前10。 - /// - public int TopN { get; init; } = 10; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetRevenueStatisticsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetRevenueStatisticsQuery.cs deleted file mode 100644 index bda07b6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetRevenueStatisticsQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; - -namespace TakeoutSaaS.Application.App.Statistics.Queries; - -/// -/// 获取收入统计。 -/// -public sealed record GetRevenueStatisticsQuery : IRequest -{ - /// - /// 统计月份数量,默认12个月。 - /// - public int MonthsCount { get; init; } = 12; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetSubscriptionOverviewQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetSubscriptionOverviewQuery.cs deleted file mode 100644 index 68b356a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetSubscriptionOverviewQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Statistics.Dto; - -namespace TakeoutSaaS.Application.App.Statistics.Queries; - -/// -/// 获取订阅概览统计。 -/// -public sealed record GetSubscriptionOverviewQuery : IRequest -{ -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchExtendSubscriptionsCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchExtendSubscriptionsCommand.cs deleted file mode 100644 index 0f2954b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchExtendSubscriptionsCommand.cs +++ /dev/null @@ -1,72 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 批量延期订阅命令。 -/// -public sealed record BatchExtendSubscriptionsCommand : IRequest -{ - /// - /// 订阅ID列表。 - /// - [Required] - [MinLength(1, ErrorMessage = "至少需要选择一个订阅")] - public IReadOnlyList SubscriptionIds { get; init; } = Array.Empty(); - - /// - /// 延期时长(天)。 - /// - [Range(1, 3650, ErrorMessage = "延期天数必须在1-3650天之间")] - public int? DurationDays { get; init; } - - /// - /// 延期时长(月)。 - /// - [Range(1, 120, ErrorMessage = "延期月数必须在1-120月之间")] - public int? DurationMonths { get; init; } - - /// - /// 备注信息。 - /// - [MaxLength(500)] - public string? Notes { get; init; } -} - -/// -/// 批量延期结果。 -/// -public record BatchExtendResult -{ - /// - /// 成功数量。 - /// - public int SuccessCount { get; init; } - - /// - /// 失败数量。 - /// - public int FailureCount { get; init; } - - /// - /// 失败详情列表。 - /// - public IReadOnlyList Failures { get; init; } = Array.Empty(); -} - -/// -/// 批量操作失败项。 -/// -public record BatchFailureItem -{ - /// - /// 订阅ID。 - /// - public long SubscriptionId { get; init; } - - /// - /// 失败原因。 - /// - public string Reason { get; init; } = string.Empty; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchSendReminderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchSendReminderCommand.cs deleted file mode 100644 index 8c41be6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchSendReminderCommand.cs +++ /dev/null @@ -1,45 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 批量发送续费提醒命令。 -/// -public sealed record BatchSendReminderCommand : IRequest -{ - /// - /// 订阅ID列表。 - /// - [Required] - [MinLength(1, ErrorMessage = "至少需要选择一个订阅")] - public IReadOnlyList SubscriptionIds { get; init; } = Array.Empty(); - - /// - /// 提醒内容。 - /// - [Required(ErrorMessage = "提醒内容不能为空")] - [MaxLength(1000, ErrorMessage = "提醒内容不能超过1000字符")] - public string ReminderContent { get; init; } = string.Empty; -} - -/// -/// 批量发送提醒结果。 -/// -public record BatchSendReminderResult -{ - /// - /// 成功发送数量。 - /// - public int SuccessCount { get; init; } - - /// - /// 发送失败数量。 - /// - public int FailureCount { get; init; } - - /// - /// 失败详情列表。 - /// - public IReadOnlyList Failures { get; init; } = Array.Empty(); -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ChangeSubscriptionPlanCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ChangeSubscriptionPlanCommand.cs deleted file mode 100644 index 3b85ae5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ChangeSubscriptionPlanCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Subscriptions.Dto; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 变更套餐命令。 -/// -public sealed record ChangeSubscriptionPlanCommand : IRequest -{ - /// - /// 订阅 ID(从路由参数绑定)。 - /// - [Required] - public long SubscriptionId { get; init; } - - /// - /// 目标套餐 ID。 - /// - [Required] - public long TargetPackageId { get; init; } - - /// - /// 是否立即生效,否则在下周期生效。 - /// - public bool Immediate { get; init; } - - /// - /// 备注信息。 - /// - [MaxLength(500)] - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ExtendSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ExtendSubscriptionCommand.cs deleted file mode 100644 index 6034e01..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ExtendSubscriptionCommand.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Subscriptions.Dto; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 延期订阅命令。 -/// -public sealed record ExtendSubscriptionCommand : IRequest -{ - /// - /// 订阅 ID(从路由参数绑定)。 - /// - [Required] - public long SubscriptionId { get; init; } - - /// - /// 延期时长(月)。 - /// - [Range(1, 120)] - public int DurationMonths { get; init; } - - /// - /// 备注信息。 - /// - [MaxLength(500)] - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessAutoRenewalCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessAutoRenewalCommand.cs deleted file mode 100644 index 50849d7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessAutoRenewalCommand.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 处理自动续费:为开启自动续费且即将到期的订阅生成续费账单。 -/// -public sealed record ProcessAutoRenewalCommand : IRequest -{ - /// - /// 到期前 N 天生成续费账单。 - /// - [Range(0, 365, ErrorMessage = "续费提前天数必须在 0~365 之间")] - public int RenewalDaysBeforeExpiry { get; init; } = 3; -} - -/// -/// 自动续费处理结果。 -/// -public sealed record ProcessAutoRenewalResult -{ - /// - /// 扫描到的候选订阅数量。 - /// - public int CandidateCount { get; init; } - - /// - /// 实际创建的账单数量。 - /// - public int CreatedBillCount { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessRenewalRemindersCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessRenewalRemindersCommand.cs deleted file mode 100644 index 701c542..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessRenewalRemindersCommand.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 处理续费提醒:按到期前指定天数批量创建站内提醒通知(幂等)。 -/// -public sealed record ProcessRenewalRemindersCommand : IRequest -{ - /// - /// 提醒时间点(到期前 N 天)。 - /// - [MinLength(1, ErrorMessage = "至少需要配置一个提醒时间点")] - public IReadOnlyList ReminderDaysBeforeExpiry { get; init; } = [7, 3, 1]; -} - -/// -/// 续费提醒处理结果。 -/// -public sealed record ProcessRenewalRemindersResult -{ - /// - /// 扫描到的候选订阅数量。 - /// - public int CandidateCount { get; init; } - - /// - /// 实际创建的提醒数量。 - /// - public int CreatedReminderCount { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessSubscriptionExpiryCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessSubscriptionExpiryCommand.cs deleted file mode 100644 index 9de607e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ProcessSubscriptionExpiryCommand.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 处理订阅到期:到期进入宽限期,宽限期结束自动暂停。 -/// -public sealed record ProcessSubscriptionExpiryCommand : IRequest -{ - /// - /// 宽限期天数。 - /// - [Range(0, 365, ErrorMessage = "宽限期天数必须在 0~365 之间")] - public int GracePeriodDays { get; init; } = 7; -} - -/// -/// 订阅到期处理结果。 -/// -public sealed record ProcessSubscriptionExpiryResult -{ - /// - /// 从 Active 进入宽限期的数量。 - /// - public int EnteredGracePeriodCount { get; init; } - - /// - /// 宽限期到期并暂停的数量。 - /// - public int SuspendedCount { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionCommand.cs deleted file mode 100644 index dd16fc5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Subscriptions.Dto; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 更新订阅基础信息命令。 -/// -public sealed record UpdateSubscriptionCommand : IRequest -{ - /// - /// 订阅 ID(从路由参数绑定)。 - /// - [Required] - public long SubscriptionId { get; init; } - - /// - /// 是否自动续费。 - /// - public bool? AutoRenew { get; init; } - - /// - /// 运营备注信息。 - /// - [MaxLength(500)] - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionStatusCommand.cs deleted file mode 100644 index 4e8240b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionStatusCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Subscriptions.Commands; - -/// -/// 更新订阅状态命令。 -/// -public sealed record UpdateSubscriptionStatusCommand : IRequest -{ - /// - /// 订阅 ID(从路由参数绑定)。 - /// - [Required] - public long SubscriptionId { get; init; } - - /// - /// 目标状态。 - /// - [Required] - public SubscriptionStatus Status { get; init; } - - /// - /// 备注信息。 - /// - [MaxLength(500)] - public string? Notes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/QuotaUsageDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/QuotaUsageDto.cs deleted file mode 100644 index 0f36ea6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/QuotaUsageDto.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Subscriptions.Dto; - -/// -/// 配额使用 DTO。 -/// -public sealed record QuotaUsageDto -{ - /// - /// 配额 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 配额上限。 - /// - public decimal LimitValue { get; init; } - - /// - /// 已使用量。 - /// - public decimal UsedValue { get; init; } - - /// - /// 使用率(百分比)。 - /// - public decimal UsagePercentage => LimitValue > 0 ? Math.Round(UsedValue / LimitValue * 100, 2) : 0; - - /// - /// 剩余额度。 - /// - public decimal RemainingValue => Math.Max(0, LimitValue - UsedValue); - - /// - /// 重置周期描述。 - /// - public string? ResetCycle { get; init; } - - /// - /// 最近一次重置时间。 - /// - public DateTime? LastResetAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionDetailDto.cs deleted file mode 100644 index 2a91720..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionDetailDto.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Subscriptions.Dto; - -/// -/// 订阅详情 DTO。 -/// -public sealed record SubscriptionDetailDto -{ - /// - /// 订阅 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 租户编码。 - /// - public string TenantCode { get; init; } = string.Empty; - - /// - /// 当前套餐 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantPackageId { get; init; } - - /// - /// 当前套餐信息。 - /// - public TenantPackageDto? Package { get; init; } - - /// - /// 排期套餐 ID(下周期生效)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long? ScheduledPackageId { get; init; } - - /// - /// 排期套餐信息。 - /// - public TenantPackageDto? ScheduledPackage { get; init; } - - /// - /// 订阅状态。 - /// - public SubscriptionStatus Status { get; init; } - - /// - /// 生效时间(UTC)。 - /// - public DateTime EffectiveFrom { get; init; } - - /// - /// 到期时间(UTC)。 - /// - public DateTime EffectiveTo { get; init; } - - /// - /// 下次计费时间。 - /// - public DateTime? NextBillingDate { get; init; } - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } - - /// - /// 配额使用情况列表。 - /// - public IReadOnlyList QuotaUsages { get; init; } = []; - - /// - /// 订阅变更历史列表。 - /// - public IReadOnlyList ChangeHistory { get; init; } = []; - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 更新时间。 - /// - public DateTime? UpdatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionHistoryDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionHistoryDto.cs deleted file mode 100644 index c513263..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionHistoryDto.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Subscriptions.Dto; - -/// -/// 订阅变更历史 DTO。 -/// -public sealed record SubscriptionHistoryDto -{ - /// - /// 历史记录 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 订阅 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantSubscriptionId { get; init; } - - /// - /// 原套餐 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long FromPackageId { get; init; } - - /// - /// 原套餐名称。 - /// - public string FromPackageName { get; init; } = string.Empty; - - /// - /// 新套餐 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long ToPackageId { get; init; } - - /// - /// 新套餐名称。 - /// - public string ToPackageName { get; init; } = string.Empty; - - /// - /// 变更类型。 - /// - public SubscriptionChangeType ChangeType { get; init; } - - /// - /// 生效时间。 - /// - public DateTime EffectiveFrom { get; init; } - - /// - /// 到期时间。 - /// - public DateTime EffectiveTo { get; init; } - - /// - /// 相关费用。 - /// - public decimal? Amount { get; init; } - - /// - /// 币种。 - /// - public string? Currency { get; init; } - - /// - /// 备注。 - /// - public string? Notes { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionListDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionListDto.cs deleted file mode 100644 index 1cb4d05..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionListDto.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Subscriptions.Dto; - -/// -/// 订阅列表 DTO。 -/// -public sealed record SubscriptionListDto -{ - /// - /// 订阅 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - public string TenantName { get; init; } = string.Empty; - - /// - /// 租户编码。 - /// - public string TenantCode { get; init; } = string.Empty; - - /// - /// 当前套餐 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantPackageId { get; init; } - - /// - /// 当前套餐名称。 - /// - public string PackageName { get; init; } = string.Empty; - - /// - /// 排期套餐 ID(下周期生效)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long? ScheduledPackageId { get; init; } - - /// - /// 排期套餐名称。 - /// - public string? ScheduledPackageName { get; init; } - - /// - /// 订阅状态。 - /// - public SubscriptionStatus Status { get; init; } - - /// - /// 生效时间(UTC)。 - /// - public DateTime EffectiveFrom { get; init; } - - /// - /// 到期时间(UTC)。 - /// - public DateTime EffectiveTo { get; init; } - - /// - /// 下次计费时间。 - /// - public DateTime? NextBillingDate { get; init; } - - /// - /// 是否自动续费。 - /// - public bool AutoRenew { get; init; } - - /// - /// 备注信息。 - /// - public string? Notes { get; init; } - - /// - /// 创建时间。 - /// - public DateTime CreatedAt { get; init; } - - /// - /// 更新时间。 - /// - public DateTime? UpdatedAt { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs deleted file mode 100644 index a617f3f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs +++ /dev/null @@ -1,135 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 批量延期订阅命令处理器。 -/// -public sealed class BatchExtendSubscriptionsCommandHandler( - ISubscriptionRepository subscriptionRepository, - IIdGenerator idGenerator, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(BatchExtendSubscriptionsCommand request, CancellationToken cancellationToken) - { - var successCount = 0; - var failures = new List(); - - // 验证参数 - if (!request.DurationDays.HasValue && !request.DurationMonths.HasValue) - { - throw new InvalidOperationException("必须指定延期天数或延期月数"); - } - - // 计算延期时间 - var extendDays = request.DurationDays ?? 0; - var extendMonths = request.DurationMonths ?? 0; - - // 查询所有订阅 - var subscriptions = await subscriptionRepository.FindByIdsAsync( - request.SubscriptionIds, - cancellationToken); - - foreach (var subscriptionId in request.SubscriptionIds) - { - try - { - var subscription = subscriptions.FirstOrDefault(s => s.Id == subscriptionId); - if (subscription == null) - { - failures.Add(new BatchFailureItem - { - SubscriptionId = subscriptionId, - Reason = "订阅不存在" - }); - continue; - } - - // 记录原始到期时间 - var originalEffectiveTo = subscription.EffectiveTo; - - // 计算新的到期时间 - var newEffectiveTo = subscription.EffectiveTo; - if (extendMonths > 0) - { - newEffectiveTo = newEffectiveTo.AddMonths(extendMonths); - } - if (extendDays > 0) - { - newEffectiveTo = newEffectiveTo.AddDays(extendDays); - } - - subscription.EffectiveTo = newEffectiveTo; - - // 更新备注 - if (!string.IsNullOrWhiteSpace(request.Notes)) - { - subscription.Notes = request.Notes; - } - - // 记录变更历史 - var history = new TenantSubscriptionHistory - { - Id = idGenerator.NextId(), - TenantId = subscription.TenantId, - TenantSubscriptionId = subscription.Id, - FromPackageId = subscription.TenantPackageId, - ToPackageId = subscription.TenantPackageId, - ChangeType = SubscriptionChangeType.Renew, - EffectiveFrom = originalEffectiveTo, - EffectiveTo = newEffectiveTo, - Amount = null, - Currency = null, - Notes = request.Notes ?? $"批量延期: {(extendMonths > 0 ? $"{extendMonths}个月" : "")}{(extendDays > 0 ? $"{extendDays}天" : "")}" - }; - - await subscriptionRepository.AddHistoryAsync(history, cancellationToken); - await subscriptionRepository.UpdateAsync(subscription, cancellationToken); - successCount++; - } - catch (Exception ex) - { - logger.LogError(ex, "批量延期订阅失败: SubscriptionId={SubscriptionId}", subscriptionId); - failures.Add(new BatchFailureItem - { - SubscriptionId = subscriptionId, - Reason = $"处理失败: {ex.Message}" - }); - } - } - - // 记录操作日志 - var operationLog = new OperationLog - { - Id = idGenerator.NextId(), - OperationType = "BatchExtend", - TargetType = "Subscription", - TargetIds = JsonSerializer.Serialize(request.SubscriptionIds), - Parameters = JsonSerializer.Serialize(new { request.DurationDays, request.DurationMonths, request.Notes }), - Result = JsonSerializer.Serialize(new { SuccessCount = successCount, FailureCount = failures.Count }), - Success = failures.Count == 0, - CreatedAt = DateTime.UtcNow - }; - - await subscriptionRepository.AddOperationLogAsync(operationLog, cancellationToken); - - // 保存所有更改 - await subscriptionRepository.SaveChangesAsync(cancellationToken); - - return new BatchExtendResult - { - SuccessCount = successCount, - FailureCount = failures.Count, - Failures = failures - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs deleted file mode 100644 index 95f36b1..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs +++ /dev/null @@ -1,104 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 批量发送续费提醒命令处理器。 -/// -public sealed class BatchSendReminderCommandHandler( - ISubscriptionRepository subscriptionRepository, - IIdGenerator idGenerator, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(BatchSendReminderCommand request, CancellationToken cancellationToken) - { - var successCount = 0; - var failures = new List(); - - // 查询所有订阅及租户信息 - var subscriptions = await subscriptionRepository.FindByIdsWithTenantAsync( - request.SubscriptionIds, - cancellationToken); - - foreach (var subscriptionId in request.SubscriptionIds) - { - try - { - var item = subscriptions.FirstOrDefault(s => s.Subscription.Id == subscriptionId); - if (item == null) - { - failures.Add(new BatchFailureItem - { - SubscriptionId = subscriptionId, - Reason = "订阅不存在" - }); - continue; - } - - // 创建通知记录 - var notification = new TenantNotification - { - Id = idGenerator.NextId(), - TenantId = item.Subscription.TenantId, - Title = "续费提醒", - Message = request.ReminderContent, - Severity = TenantNotificationSeverity.Warning, - Channel = TenantNotificationChannel.InApp, - SentAt = DateTime.UtcNow, - ReadAt = null, - CreatedAt = DateTime.UtcNow - }; - - await subscriptionRepository.AddNotificationAsync(notification, cancellationToken); - successCount++; - - logger.LogInformation( - "发送续费提醒: SubscriptionId={SubscriptionId}, TenantId={TenantId}, TenantName={TenantName}", - subscriptionId, item.Subscription.TenantId, item.Tenant.Name); - } - catch (Exception ex) - { - logger.LogError(ex, "发送续费提醒失败: SubscriptionId={SubscriptionId}", subscriptionId); - failures.Add(new BatchFailureItem - { - SubscriptionId = subscriptionId, - Reason = $"发送失败: {ex.Message}" - }); - } - } - - // 记录操作日志 - var operationLog = new OperationLog - { - Id = idGenerator.NextId(), - OperationType = "BatchRemind", - TargetType = "Subscription", - TargetIds = JsonSerializer.Serialize(request.SubscriptionIds), - Parameters = JsonSerializer.Serialize(new { request.ReminderContent }), - Result = JsonSerializer.Serialize(new { SuccessCount = successCount, FailureCount = failures.Count }), - Success = failures.Count == 0, - CreatedAt = DateTime.UtcNow - }; - - await subscriptionRepository.AddOperationLogAsync(operationLog, cancellationToken); - - // 保存所有更改 - await subscriptionRepository.SaveChangesAsync(cancellationToken); - - return new BatchSendReminderResult - { - SuccessCount = successCount, - FailureCount = failures.Count, - Failures = failures - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs deleted file mode 100644 index b744a34..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs +++ /dev/null @@ -1,95 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Application.App.Subscriptions.Queries; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 变更套餐命令处理器。 -/// -public sealed class ChangeSubscriptionPlanCommandHandler( - ISubscriptionRepository subscriptionRepository, - IIdGenerator idGenerator, - IMediator mediator) - : IRequestHandler -{ - /// - public async Task Handle(ChangeSubscriptionPlanCommand request, CancellationToken cancellationToken) - { - // 1. 查询订阅 - var subscription = await subscriptionRepository.FindByIdAsync( - request.SubscriptionId, - cancellationToken); - - if (subscription == null) - { - return null; - } - - // 2. 记录原套餐ID - var previousPackageId = subscription.TenantPackageId; - - // 3. 根据是否立即生效更新订阅 - if (request.Immediate) - { - // 立即生效:直接更新当前套餐 - subscription.TenantPackageId = request.TargetPackageId; - subscription.ScheduledPackageId = null; - } - else - { - // 下周期生效:设置排期套餐 - subscription.ScheduledPackageId = request.TargetPackageId; - } - - // 4. 更新备注 - if (!string.IsNullOrWhiteSpace(request.Notes)) - { - subscription.Notes = request.Notes; - } - - // 5. 判断变更类型(升级或降级) - var fromPackage = await subscriptionRepository.FindPackageByIdAsync(previousPackageId, cancellationToken); - var toPackage = await subscriptionRepository.FindPackageByIdAsync(request.TargetPackageId, cancellationToken); - - var changeType = SubscriptionChangeType.Upgrade; - if (fromPackage != null && toPackage != null) - { - // 简单根据价格判断升降级 - if (toPackage.MonthlyPrice < fromPackage.MonthlyPrice) - { - changeType = SubscriptionChangeType.Downgrade; - } - } - - // 6. 记录变更历史 - var history = new TenantSubscriptionHistory - { - Id = idGenerator.NextId(), - TenantId = subscription.TenantId, - TenantSubscriptionId = subscription.Id, - FromPackageId = previousPackageId, - ToPackageId = request.TargetPackageId, - ChangeType = changeType, - EffectiveFrom = request.Immediate ? DateTime.UtcNow : subscription.EffectiveTo, - EffectiveTo = subscription.EffectiveTo, - Amount = null, - Currency = null, - Notes = request.Notes ?? (request.Immediate ? "套餐立即变更" : "套餐排期变更") - }; - - await subscriptionRepository.AddHistoryAsync(history, cancellationToken); - - // 7. 保存更改 - await subscriptionRepository.UpdateAsync(subscription, cancellationToken); - await subscriptionRepository.SaveChangesAsync(cancellationToken); - - // 8. 返回更新后的详情 - return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs deleted file mode 100644 index 2717c86..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Application.App.Subscriptions.Queries; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 延期订阅命令处理器。 -/// -public sealed class ExtendSubscriptionCommandHandler( - ISubscriptionRepository subscriptionRepository, - IIdGenerator idGenerator, - IMediator mediator) - : IRequestHandler -{ - /// - public async Task Handle(ExtendSubscriptionCommand request, CancellationToken cancellationToken) - { - // 1. 查询订阅 - var subscription = await subscriptionRepository.FindByIdAsync( - request.SubscriptionId, - cancellationToken); - - if (subscription == null) - { - return null; - } - - // 2. 计算新的到期时间(从当前到期时间延长) - var originalEffectiveTo = subscription.EffectiveTo; - subscription.EffectiveTo = subscription.EffectiveTo.AddMonths(request.DurationMonths); - - // 3. 更新备注 - if (!string.IsNullOrWhiteSpace(request.Notes)) - { - subscription.Notes = request.Notes; - } - - // 4. 记录变更历史(使用 Renew 类型表示延期) - var history = new TenantSubscriptionHistory - { - Id = idGenerator.NextId(), - TenantId = subscription.TenantId, - TenantSubscriptionId = subscription.Id, - FromPackageId = subscription.TenantPackageId, - ToPackageId = subscription.TenantPackageId, - ChangeType = SubscriptionChangeType.Renew, - EffectiveFrom = originalEffectiveTo, - EffectiveTo = subscription.EffectiveTo, - Amount = null, - Currency = null, - Notes = request.Notes ?? $"延期 {request.DurationMonths} 个月" - }; - - await subscriptionRepository.AddHistoryAsync(history, cancellationToken); - - // 5. 保存更改 - await subscriptionRepository.UpdateAsync(subscription, cancellationToken); - await subscriptionRepository.SaveChangesAsync(cancellationToken); - - // 6. 返回更新后的详情 - return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs deleted file mode 100644 index d4c3c0d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs +++ /dev/null @@ -1,140 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Application.App.Subscriptions.Queries; -using TakeoutSaaS.Application.App.Tenants; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 订阅详情查询处理器。 -/// -public sealed class GetSubscriptionDetailQueryHandler( - ISubscriptionRepository subscriptionRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetSubscriptionDetailQuery request, CancellationToken cancellationToken) - { - // 1. 查询订阅基础信息 - var detail = await subscriptionRepository.GetDetailAsync( - request.SubscriptionId, - cancellationToken, - includeDeleted: request.IncludeDeleted); - - if (detail == null) - { - return null; - } - - // 2. 查询配额使用情况 - var quotaUsages = await subscriptionRepository.GetQuotaUsagesAsync( - detail.Subscription.TenantId, - cancellationToken, - includeDeleted: request.IncludeDeleted); - - var quotaUsageDtos = BuildQuotaUsageDtos(detail.Package, quotaUsages); - - // 3. 查询订阅变更历史(关联套餐信息) - var histories = await subscriptionRepository.GetHistoryAsync(request.SubscriptionId, cancellationToken); - - var historyDtos = histories.Select(h => new SubscriptionHistoryDto - { - Id = h.History.Id, - TenantSubscriptionId = h.History.TenantSubscriptionId, - FromPackageId = h.History.FromPackageId, - FromPackageName = h.FromPackageName, - ToPackageId = h.History.ToPackageId, - ToPackageName = h.ToPackageName, - ChangeType = h.History.ChangeType, - EffectiveFrom = h.History.EffectiveFrom, - EffectiveTo = h.History.EffectiveTo, - Amount = h.History.Amount, - Currency = h.History.Currency, - Notes = h.History.Notes, - CreatedAt = h.History.CreatedAt - }).ToList(); - - // 4. 构建返回结果 - return new SubscriptionDetailDto - { - Id = detail.Subscription.Id, - TenantId = detail.Subscription.TenantId, - TenantName = detail.TenantName, - TenantCode = detail.TenantCode, - TenantPackageId = detail.Subscription.TenantPackageId, - Package = detail.Package?.ToDto(), - ScheduledPackageId = detail.Subscription.ScheduledPackageId, - ScheduledPackage = detail.ScheduledPackage?.ToDto(), - Status = detail.Subscription.Status, - EffectiveFrom = detail.Subscription.EffectiveFrom, - EffectiveTo = detail.Subscription.EffectiveTo, - NextBillingDate = detail.Subscription.NextBillingDate, - AutoRenew = detail.Subscription.AutoRenew, - Notes = detail.Subscription.Notes, - QuotaUsages = quotaUsageDtos, - ChangeHistory = historyDtos, - CreatedAt = detail.Subscription.CreatedAt, - UpdatedAt = detail.Subscription.UpdatedAt - }; - } - - private static List BuildQuotaUsageDtos( - TakeoutSaaS.Domain.Tenants.Entities.TenantPackage? package, - IReadOnlyList quotaUsages) - { - var usageByType = quotaUsages - .GroupBy(u => u.QuotaType) - .ToDictionary(g => g.Key, g => g.First()); - - var baselineTypes = new List<(TenantQuotaType Type, decimal LimitValue)>(); - if (package != null) - { - baselineTypes.Add((TenantQuotaType.StoreCount, package.MaxStoreCount.HasValue ? package.MaxStoreCount.Value : 0)); - baselineTypes.Add((TenantQuotaType.AccountCount, package.MaxAccountCount.HasValue ? package.MaxAccountCount.Value : 0)); - baselineTypes.Add((TenantQuotaType.Storage, package.MaxStorageGb.HasValue ? package.MaxStorageGb.Value : 0)); - baselineTypes.Add((TenantQuotaType.SmsCredits, package.MaxSmsCredits.HasValue ? package.MaxSmsCredits.Value : 0)); - baselineTypes.Add((TenantQuotaType.DeliveryOrders, package.MaxDeliveryOrders.HasValue ? package.MaxDeliveryOrders.Value : 0)); - } - - var results = new List(); - - foreach (var (type, limitValue) in baselineTypes) - { - usageByType.TryGetValue(type, out var usage); - results.Add(new QuotaUsageDto - { - Id = usage?.Id ?? 0, - QuotaType = type, - LimitValue = limitValue, - UsedValue = usage?.UsedValue ?? 0, - ResetCycle = usage?.ResetCycle, - LastResetAt = usage?.LastResetAt - }); - } - - // 补充套餐字段未覆盖的配额使用项(例如营销槽位等扩展配额) - foreach (var usage in usageByType.Values) - { - if (baselineTypes.Any(x => x.Type == usage.QuotaType)) - { - continue; - } - - results.Add(new QuotaUsageDto - { - Id = usage.Id, - QuotaType = usage.QuotaType, - LimitValue = usage.LimitValue, - UsedValue = usage.UsedValue, - ResetCycle = usage.ResetCycle, - LastResetAt = usage.LastResetAt - }); - } - - return results - .OrderBy(x => (int)x.QuotaType) - .ToList(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs deleted file mode 100644 index 132ff51..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Application.App.Subscriptions.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 订阅分页查询处理器。 -/// -public sealed class GetSubscriptionListQueryHandler( - ISubscriptionRepository subscriptionRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(GetSubscriptionListQuery request, CancellationToken cancellationToken) - { - // 1. 构建查询过滤条件 - var filter = new SubscriptionSearchFilter - { - Status = request.Status, - TenantPackageId = request.TenantPackageId, - TenantId = request.TenantId, - TenantKeyword = request.TenantKeyword, - ExpiringWithinDays = request.ExpiringWithinDays, - AutoRenew = request.AutoRenew, - Page = request.Page, - PageSize = request.PageSize - }; - - // 2. 执行分页查询 - var (items, total) = await subscriptionRepository.SearchPagedAsync( - filter, - cancellationToken, - includeDeleted: request.IncludeDeleted); - - // 3. 映射为 DTO - var dtos = items.Select(x => new SubscriptionListDto - { - Id = x.Subscription.Id, - TenantId = x.Subscription.TenantId, - TenantName = x.TenantName, - TenantCode = x.TenantCode, - TenantPackageId = x.Subscription.TenantPackageId, - PackageName = x.PackageName, - ScheduledPackageId = x.Subscription.ScheduledPackageId, - ScheduledPackageName = x.ScheduledPackageName, - Status = x.Subscription.Status, - EffectiveFrom = x.Subscription.EffectiveFrom, - EffectiveTo = x.Subscription.EffectiveTo, - NextBillingDate = x.Subscription.NextBillingDate, - AutoRenew = x.Subscription.AutoRenew, - Notes = x.Subscription.Notes, - CreatedAt = x.Subscription.CreatedAt, - UpdatedAt = x.Subscription.UpdatedAt - }).ToList(); - - // 4. 返回分页结果 - return new PagedResult(dtos, request.Page, request.PageSize, total); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs deleted file mode 100644 index 960ed10..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs +++ /dev/null @@ -1,137 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 自动续费处理器:生成续费账单(幂等)。 -/// -public sealed class ProcessAutoRenewalCommandHandler( - ISubscriptionRepository subscriptionRepository, - ITenantBillingRepository billingRepository, - IIdGenerator idGenerator, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(ProcessAutoRenewalCommand request, CancellationToken cancellationToken) - { - // 1. 计算续费阈值时间 - var now = DateTime.UtcNow; - var renewalThreshold = now.AddDays(request.RenewalDaysBeforeExpiry); - - // 2. 查询候选订阅(含套餐) - var candidates = await subscriptionRepository.FindAutoRenewalCandidatesAsync( - now, - renewalThreshold, - cancellationToken); - var createdBillCount = 0; - - // 3. 遍历候选订阅,生成账单 - foreach (var candidate in candidates) - { - // 3.1 幂等校验:同一周期开始时间只允许存在一张未取消账单 - var periodStart = candidate.Subscription.EffectiveTo; - var exists = await billingRepository.ExistsNotCancelledByPeriodStartAsync( - candidate.Subscription.TenantId, - periodStart, - cancellationToken); - - if (exists) - { - logger.LogInformation( - "自动续费:租户 {TenantId} 订阅 {SubscriptionId} 已存在周期 {PeriodStart} 的续费账单,跳过", - candidate.Subscription.TenantId, - candidate.Subscription.Id, - periodStart); - continue; - } - - // 3.2 计算续费周期(月) - var durationMonths = CalculateDurationMonths(candidate.Subscription.EffectiveFrom, candidate.Subscription.EffectiveTo); - if (durationMonths <= 0) - { - durationMonths = 1; - } - - // 3.3 计算账单周期与金额 - var periodEnd = periodStart.AddMonths(durationMonths); - var amountDue = CalculateRenewalAmount(candidate.Package, durationMonths); - - // 3.4 生成账单(Pending) - var statementNo = $"BILL-{now:yyyyMMddHHmmss}-{candidate.Subscription.TenantId}-{candidate.Subscription.Id}"; - var lineItemsJson = JsonSerializer.Serialize(new - { - PackageName = candidate.Package.Name, - RenewalMonths = durationMonths, - SubscriptionId = candidate.Subscription.Id - }); - - await billingRepository.AddAsync(new TenantBillingStatement - { - Id = idGenerator.NextId(), - TenantId = candidate.Subscription.TenantId, - StatementNo = statementNo, - PeriodStart = periodStart, - PeriodEnd = periodEnd, - AmountDue = amountDue, - AmountPaid = 0, - DueDate = periodStart.AddDays(-1), - LineItemsJson = lineItemsJson, - CreatedAt = now - }, cancellationToken); - - createdBillCount++; - logger.LogInformation( - "自动续费:为租户 {TenantId} 订阅 {SubscriptionId} 生成账单 {StatementNo},金额 {AmountDue},周期 {PeriodStart}~{PeriodEnd}", - candidate.Subscription.TenantId, - candidate.Subscription.Id, - statementNo, - amountDue, - periodStart, - periodEnd); - } - - // 4. 保存账单变更 - if (createdBillCount > 0) - { - await billingRepository.SaveChangesAsync(cancellationToken); - } - - return new ProcessAutoRenewalResult - { - CandidateCount = candidates.Count, - CreatedBillCount = createdBillCount - }; - } - - private static int CalculateDurationMonths(DateTime effectiveFrom, DateTime effectiveTo) - { - // 1. 以年月差作为周期(月),兼容“按月续费”模型 - var months = (effectiveTo.Year - effectiveFrom.Year) * 12 + effectiveTo.Month - effectiveFrom.Month; - - // 2. 对不足 1 个月的情况兜底为 1 - return months <= 0 ? 1 : months; - } - - private static decimal CalculateRenewalAmount(TenantPackage package, int durationMonths) - { - // 1. 优先使用年付价(按整年计费),剩余月份按月付价补齐 - var monthlyPrice = package.MonthlyPrice ?? 0m; - var yearlyPrice = package.YearlyPrice; - if (yearlyPrice is null || durationMonths < 12) - { - return monthlyPrice * durationMonths; - } - - // 2. 按年 + 月组合计算金额 - var years = durationMonths / 12; - var remainingMonths = durationMonths % 12; - return yearlyPrice.Value * years + monthlyPrice * remainingMonths; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs deleted file mode 100644 index f1c1aba..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs +++ /dev/null @@ -1,117 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 续费提醒处理器:在到期前 7/3/1 天等时间点发送站内提醒(幂等)。 -/// -public sealed class ProcessRenewalRemindersCommandHandler( - ISubscriptionRepository subscriptionRepository, - ITenantNotificationRepository notificationRepository, - IIdGenerator idGenerator, - ILogger logger) - : IRequestHandler -{ - private const string ReminderTitle = "订阅续费提醒"; - - /// - public async Task Handle(ProcessRenewalRemindersCommand request, CancellationToken cancellationToken) - { - // 1. 读取提醒配置 - var now = DateTime.UtcNow; - var candidateCount = 0; - var createdReminderCount = 0; - var dedupeWindowStart = now.AddHours(-24); - - // 2. 按提醒时间点扫描到期订阅 - foreach (var daysBeforeExpiry in request.ReminderDaysBeforeExpiry.Distinct().OrderByDescending(x => x)) - { - // 2.1 计算目标日期区间(按天匹配) - var targetDate = now.AddDays(daysBeforeExpiry); - var startOfDay = targetDate.Date; - var endOfDay = startOfDay.AddDays(1); - - // 2.2 查询候选订阅(活跃 + 未开自动续费 + 到期在当日) - var candidates = await subscriptionRepository.FindRenewalReminderCandidatesAsync( - startOfDay, - endOfDay, - cancellationToken); - candidateCount += candidates.Count; - - foreach (var item in candidates) - { - // 2.3 幂等:同一订阅 + 同一天数提醒,在 24 小时内只发送一次 - var metadataJson = BuildReminderMetadata(item.Subscription.Id, daysBeforeExpiry, item.Subscription.EffectiveTo); - var alreadySent = await notificationRepository.ExistsByMetadataAsync( - item.Subscription.TenantId, - ReminderTitle, - metadataJson, - dedupeWindowStart, - cancellationToken); - - if (alreadySent) - { - continue; - } - - // 2.4 构造提醒内容并入库 - var notification = new TenantNotification - { - Id = idGenerator.NextId(), - TenantId = item.Subscription.TenantId, - Title = ReminderTitle, - Message = $"您的订阅套餐「{item.Package.Name}」将在 {daysBeforeExpiry} 天内到期(到期时间:{item.Subscription.EffectiveTo:yyyy-MM-dd HH:mm}),请及时续费以免影响使用。", - Severity = daysBeforeExpiry <= 1 - ? TenantNotificationSeverity.Critical - : TenantNotificationSeverity.Warning, - Channel = TenantNotificationChannel.InApp, - SentAt = now, - ReadAt = null, - MetadataJson = metadataJson, - CreatedAt = now - }; - - await notificationRepository.AddAsync(notification, cancellationToken); - createdReminderCount++; - - logger.LogInformation( - "续费提醒:TenantId={TenantId}, TenantName={TenantName}, SubscriptionId={SubscriptionId}, DaysBeforeExpiry={DaysBeforeExpiry}", - item.Tenant.Id, - item.Tenant.Name, - item.Subscription.Id, - daysBeforeExpiry); - } - } - - // 3. 保存变更 - if (createdReminderCount > 0) - { - await notificationRepository.SaveChangesAsync(cancellationToken); - } - - return new ProcessRenewalRemindersResult - { - CandidateCount = candidateCount, - CreatedReminderCount = createdReminderCount - }; - } - - private static string BuildReminderMetadata(long subscriptionId, int daysBeforeExpiry, DateTime effectiveTo) - { - // 1. 使用稳定字段顺序的 JSON 作为幂等键 - return JsonSerializer.Serialize(new - { - Type = "RenewalReminder", - SubscriptionId = subscriptionId, - DaysBeforeExpiry = daysBeforeExpiry, - EffectiveTo = effectiveTo.ToString("O") - }); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs deleted file mode 100644 index 9b655e5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 订阅到期处理器:自动进入宽限期并在宽限期结束后暂停。 -/// -public sealed class ProcessSubscriptionExpiryCommandHandler( - ISubscriptionRepository subscriptionRepository, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(ProcessSubscriptionExpiryCommand request, CancellationToken cancellationToken) - { - // 1. 查询到期订阅 - var now = DateTime.UtcNow; - var expiredActive = await subscriptionRepository.FindExpiredActiveSubscriptionsAsync( - now, - cancellationToken); - var gracePeriodExpired = await subscriptionRepository.FindGracePeriodExpiredSubscriptionsAsync( - now, - request.GracePeriodDays, - cancellationToken); - - // 2. 更新订阅状态 - foreach (var subscription in expiredActive) - { - subscription.Status = SubscriptionStatus.GracePeriod; - await subscriptionRepository.UpdateAsync(subscription, cancellationToken); - } - - // 3. 宽限期到期自动暂停 - foreach (var subscription in gracePeriodExpired) - { - subscription.Status = SubscriptionStatus.Suspended; - await subscriptionRepository.UpdateAsync(subscription, cancellationToken); - } - - // 4. 保存变更 - var totalChanged = expiredActive.Count + gracePeriodExpired.Count; - if (totalChanged > 0) - { - await subscriptionRepository.SaveChangesAsync(cancellationToken); - } - - logger.LogInformation( - "订阅到期处理完成:进入宽限期 {EnteredGracePeriodCount},暂停 {SuspendedCount},宽限期天数 {GracePeriodDays}", - expiredActive.Count, - gracePeriodExpired.Count, - request.GracePeriodDays); - - return new ProcessSubscriptionExpiryResult - { - EnteredGracePeriodCount = expiredActive.Count, - SuspendedCount = gracePeriodExpired.Count - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs deleted file mode 100644 index 70c852c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Application.App.Subscriptions.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 更新订阅基础信息命令处理器。 -/// -public sealed class UpdateSubscriptionCommandHandler( - ISubscriptionRepository subscriptionRepository, - IMediator mediator) - : IRequestHandler -{ - /// - public async Task Handle(UpdateSubscriptionCommand request, CancellationToken cancellationToken) - { - // 1. 查询订阅 - var subscription = await subscriptionRepository.FindByIdAsync( - request.SubscriptionId, - cancellationToken); - - if (subscription == null) - { - return null; - } - - // 2. 更新字段 - if (request.AutoRenew.HasValue) - { - subscription.AutoRenew = request.AutoRenew.Value; - } - - if (request.Notes != null) - { - subscription.Notes = request.Notes; - } - - // 3. 保存更改 - await subscriptionRepository.UpdateAsync(subscription, cancellationToken); - await subscriptionRepository.SaveChangesAsync(cancellationToken); - - // 4. 返回更新后的详情 - return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs deleted file mode 100644 index 58c2652..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Application.App.Subscriptions.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; - -/// -/// 更新订阅状态命令处理器。 -/// -public sealed class UpdateSubscriptionStatusCommandHandler( - ISubscriptionRepository subscriptionRepository, - IMediator mediator) - : IRequestHandler -{ - /// - public async Task Handle(UpdateSubscriptionStatusCommand request, CancellationToken cancellationToken) - { - // 1. 查询订阅 - var subscription = await subscriptionRepository.FindByIdAsync( - request.SubscriptionId, - cancellationToken); - - if (subscription == null) - { - return null; - } - - // 2. 更新状态 - subscription.Status = request.Status; - - // 3. 更新备注 - if (!string.IsNullOrWhiteSpace(request.Notes)) - { - subscription.Notes = request.Notes; - } - - // 4. 保存更改 - await subscriptionRepository.UpdateAsync(subscription, cancellationToken); - await subscriptionRepository.SaveChangesAsync(cancellationToken); - - // 5. 返回更新后的详情 - return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs deleted file mode 100644 index c602a31..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Dto; - -namespace TakeoutSaaS.Application.App.Subscriptions.Queries; - -/// -/// 查询订阅详情(含套餐信息、配额使用、变更历史)。 -/// -public sealed record GetSubscriptionDetailQuery : IRequest -{ - /// - /// 订阅 ID。 - /// - public long SubscriptionId { get; init; } - - /// - /// 是否包含已软删除数据。 - /// - public bool IncludeDeleted { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs deleted file mode 100644 index 3c83c3d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs +++ /dev/null @@ -1,57 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Subscriptions.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Subscriptions.Queries; - -/// -/// 订阅分页查询。 -/// -public sealed record GetSubscriptionListQuery : IRequest> -{ - /// - /// 订阅状态(精确匹配)。 - /// - public SubscriptionStatus? Status { get; init; } - - /// - /// 套餐 ID(精确匹配)。 - /// - public long? TenantPackageId { get; init; } - - /// - /// 租户 ID(精确匹配)。 - /// - public long? TenantId { get; init; } - - /// - /// 租户关键词(名称或编码模糊匹配)。 - /// - public string? TenantKeyword { get; init; } - - /// - /// 到期时间筛选:未来 N 天内到期。 - /// - public int? ExpiringWithinDays { get; init; } - - /// - /// 是否自动续费筛选。 - /// - public bool? AutoRenew { get; init; } - - /// - /// 是否包含已软删除数据。 - /// - public bool IncludeDeleted { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页大小。 - /// - public int PageSize { get; init; } = 20; -} 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/CheckTenantQuotaCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CheckTenantQuotaCommand.cs deleted file mode 100644 index 3941e81..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CheckTenantQuotaCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 校验并消费租户配额命令。 -/// -public sealed record CheckTenantQuotaCommand : IRequest -{ - /// - /// 目标租户 ID。 - /// - public long TenantId { get; init; } - - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 本次申请使用量。 - /// - public decimal Delta { get; init; } = 1; -} 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/CreateTenantAnnouncementCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAnnouncementCommand.cs deleted file mode 100644 index ad5e471..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAnnouncementCommand.cs +++ /dev/null @@ -1,67 +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 CreateTenantAnnouncementCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 公告标题。 - /// - [Required] - [StringLength(128)] - public string Title { get; init; } = string.Empty; - - /// - /// 公告正文内容。 - /// - [Required] - public string Content { get; init; } = string.Empty; - - /// - /// 公告类型。 - /// - public TenantAnnouncementType AnnouncementType { get; init; } = TenantAnnouncementType.System; - - /// - /// 优先级,数值越大越靠前。 - /// - public int Priority { get; init; } = 0; - - /// - /// 生效开始时间(UTC)。 - /// - public DateTime EffectiveFrom { get; init; } = DateTime.UtcNow; - - /// - /// 生效结束时间(UTC),为空则长期有效。 - /// - public DateTime? EffectiveTo { get; init; } - - /// - /// 发布者范围。 - /// - public PublisherScope PublisherScope { get; init; } = PublisherScope.Tenant; - - /// - /// 目标受众类型。 - /// - [Required] - [MaxLength(64)] - public string TargetType { get; init; } = string.Empty; - - /// - /// 目标受众参数(JSON)。 - /// - public string? TargetParameters { 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/DeleteTenantAnnouncementCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantAnnouncementCommand.cs deleted file mode 100644 index 3cd5964..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantAnnouncementCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 删除租户公告命令。 -/// -public sealed record DeleteTenantAnnouncementCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 公告 ID。 - /// - public long AnnouncementId { 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/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/MarkTenantNotificationReadCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantNotificationReadCommand.cs deleted file mode 100644 index 149a74a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantNotificationReadCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 标记通知已读命令。 -/// -public sealed record MarkTenantNotificationReadCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 通知 ID。 - /// - public long NotificationId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.cs deleted file mode 100644 index ce3281a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.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 PublishAnnouncementCommand : IRequest -{ - /// - /// 租户 ID(0 表示平台公告)。 - /// - [Range(0, long.MaxValue)] - public long TenantId { get; init; } - - /// - /// 公告 ID。 - /// - [Range(1, long.MaxValue)] - public long AnnouncementId { get; init; } - - /// - /// 并发控制版本。 - /// - [Required] - [MinLength(1)] - public byte[] RowVersion { get; init; } = Array.Empty(); -} 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/RevokeAnnouncementCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RevokeAnnouncementCommand.cs deleted file mode 100644 index 09b1a79..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RevokeAnnouncementCommand.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 RevokeAnnouncementCommand : IRequest -{ - /// - /// 租户 ID(0 表示平台公告)。 - /// - [Range(0, long.MaxValue)] - public long TenantId { get; init; } - - /// - /// 公告 ID。 - /// - [Range(1, long.MaxValue)] - public long AnnouncementId { get; init; } - - /// - /// 并发控制版本。 - /// - [Required] - [MinLength(1)] - public byte[] RowVersion { get; init; } = Array.Empty(); -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs deleted file mode 100644 index 94de46d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs +++ /dev/null @@ -1,67 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 提交租户实名认证资料。 -/// -public sealed record SubmitTenantVerificationCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } - - /// - /// 营业执照编号。 - /// - 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; } -} 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/UpdateTenantAnnouncementCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantAnnouncementCommand.cs deleted file mode 100644 index 9ff5291..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantAnnouncementCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 更新租户公告命令。 -/// -public sealed record UpdateTenantAnnouncementCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 公告 ID。 - /// - public long AnnouncementId { get; init; } - - /// - /// 公告标题。 - /// - [Required] - [StringLength(128)] - public string Title { get; init; } = string.Empty; - - /// - /// 公告内容。 - /// - [Required] - public string Content { get; init; } = string.Empty; - - /// - /// 目标受众类型。 - /// - [Required] - [MaxLength(64)] - public string TargetType { get; init; } = string.Empty; - - /// - /// 目标受众参数(JSON)。 - /// - public string? TargetParameters { get; init; } - - /// - /// 并发控制版本。 - /// - [Required] - [MinLength(1)] - public byte[] RowVersion { get; init; } = Array.Empty(); -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantCommand.cs deleted file mode 100644 index dcc5cda..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 更新租户基础信息命令。 -/// -public sealed record UpdateTenantCommand : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 租户名称。 - /// - [Required] - [StringLength(128)] - public string Name { get; init; } = string.Empty; - - /// - /// 租户简称。 - /// - [StringLength(64)] - public string? ShortName { get; init; } - - /// - /// 所属行业。 - /// - [StringLength(64)] - public string? Industry { get; init; } - - /// - /// 联系人姓名。 - /// - [StringLength(64)] - public string? ContactName { get; init; } - - /// - /// 联系人电话。 - /// - [StringLength(32)] - public string? ContactPhone { get; init; } - - /// - /// 联系人邮箱。 - /// - [StringLength(128)] - public string? ContactEmail { 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/QuotaCheckResultDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/QuotaCheckResultDto.cs deleted file mode 100644 index b9ce7e6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/QuotaCheckResultDto.cs +++ /dev/null @@ -1,29 +0,0 @@ -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 配额校验结果。 -/// -public sealed class QuotaCheckResultDto -{ - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 当前配额上限,null 表示无限制。 - /// - public decimal? Limit { get; init; } - - /// - /// 已使用数量。 - /// - public decimal Used { get; init; } - - /// - /// 剩余额度,null 表示无限制。 - /// - public decimal? Remaining { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/QuotaUsageHistoryDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/QuotaUsageHistoryDto.cs deleted file mode 100644 index bff6e51..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/QuotaUsageHistoryDto.cs +++ /dev/null @@ -1,45 +0,0 @@ -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 租户配额使用历史 DTO。 -/// -public sealed record QuotaUsageHistoryDto -{ - /// - /// 配额类型。 - /// - public TenantQuotaType QuotaType { get; init; } - - /// - /// 已使用值。 - /// - public decimal UsedValue { get; init; } - - /// - /// 限额值。 - /// - public decimal LimitValue { get; init; } - - /// - /// 记录时间(UTC)。 - /// - public DateTime RecordedAt { get; init; } - - /// - /// 变更类型:increase | decrease | init | snapshot。 - /// - public string ChangeType { get; init; } = "snapshot"; - - /// - /// 变更量(可选)。 - /// - public decimal? ChangeAmount { get; init; } - - /// - /// 变更原因(可选)。 - /// - public string? ChangeReason { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAnnouncementDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAnnouncementDto.cs deleted file mode 100644 index 369d85a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAnnouncementDto.cs +++ /dev/null @@ -1,113 +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 TenantAnnouncementDto -{ - /// - /// 公告 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 公告标题。 - /// - public string Title { get; init; } = string.Empty; - - /// - /// 公告正文内容。 - /// - public string Content { get; init; } = string.Empty; - - /// - /// 公告类型。 - /// - public TenantAnnouncementType AnnouncementType { get; init; } - - /// - /// 优先级,数值越大越靠前。 - /// - public int Priority { get; init; } - - /// - /// 生效开始时间(UTC)。 - /// - public DateTime EffectiveFrom { get; init; } - - /// - /// 生效结束时间(UTC),为空则长期有效。 - /// - public DateTime? EffectiveTo { get; init; } - - /// - /// 发布者范围。 - /// - public PublisherScope PublisherScope { get; init; } - - /// - /// 发布者用户 ID。 - /// - public long? PublisherUserId { get; init; } - - /// - /// 公告状态。 - /// - public AnnouncementStatus Status { get; init; } - - /// - /// 实际发布时间(UTC)。 - /// - public DateTime? PublishedAt { get; init; } - - /// - /// 撤销时间(UTC)。 - /// - public DateTime? RevokedAt { get; init; } - - /// - /// 预定发布时间(UTC)。 - /// - public DateTime? ScheduledPublishAt { get; init; } - - /// - /// 目标受众类型。 - /// - public string TargetType { get; init; } = string.Empty; - - /// - /// 目标受众参数(JSON)。 - /// - public string? TargetParameters { get; init; } - - /// - /// 并发控制版本。 - /// - public byte[] RowVersion { get; init; } = Array.Empty(); - - /// - /// 是否启用(迁移期保留)。 - /// - public bool IsActive { get; init; } - - /// - /// 当前用户是否已读。 - /// - public bool IsRead { get; init; } - - /// - /// 已读时间(UTC)。 - /// - public DateTime? ReadAt { get; init; } -} 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/TenantBillingDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantBillingDto.cs deleted file mode 100644 index 1007224..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantBillingDto.cs +++ /dev/null @@ -1,63 +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 TenantBillingDto -{ - /// - /// 账单 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - 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; } - - /// - /// 到期日(UTC)。 - /// - public DateTime DueDate { get; init; } - - /// - /// 账单明细 JSON。 - /// - public string? LineItemsJson { 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/TenantNotificationDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantNotificationDto.cs deleted file mode 100644 index 9a33244..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantNotificationDto.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 TenantNotificationDto -{ - /// - /// 通知 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户 ID(雪花算法,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 通知标题。 - /// - public string Title { get; init; } = string.Empty; - - /// - /// 通知内容。 - /// - public string Message { get; init; } = string.Empty; - - /// - /// 通道类型(如站内信、短信、邮件)。 - /// - public TenantNotificationChannel Channel { get; init; } - - /// - /// 通知等级。 - /// - public TenantNotificationSeverity Severity { get; init; } - - /// - /// 发送时间(UTC)。 - /// - public DateTime SentAt { get; init; } - - /// - /// 阅读时间(UTC)。 - /// - public DateTime? ReadAt { get; init; } - - /// - /// 附加元数据 JSON。 - /// - public string? MetadataJson { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageDto.cs deleted file mode 100644 index e901d5e..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageDto.cs +++ /dev/null @@ -1,107 +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 TenantPackageDto -{ - /// - /// 套餐 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 套餐名称。 - /// - public string Name { get; init; } = string.Empty; - - /// - /// 描述。 - /// - public string? Description { get; init; } - - /// - /// 套餐类型。 - /// - public TenantPackageType PackageType { get; init; } - - /// - /// 月付价格。 - /// - 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; } - - /// - /// 是否对外可见。 - /// - public bool IsPublicVisible { get; init; } - - /// - /// 是否允许新租户购买/选择。 - /// - public bool IsAllowNewTenantPurchase { get; init; } - - /// - /// 发布状态。 - /// - public TenantPackagePublishStatus PublishStatus { get; init; } - - /// - /// 是否推荐展示(运营推荐标识)。 - /// - public bool IsRecommended { get; init; } - - /// - /// 套餐标签(用于展示与对比页)。 - /// - public string[] Tags { get; init; } = []; - - /// - /// 展示排序,数值越小越靠前。 - /// - public int SortOrder { 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/Dto/TenantVerificationDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantVerificationDto.cs deleted file mode 100644 index f925008..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantVerificationDto.cs +++ /dev/null @@ -1,104 +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 TenantVerificationDto -{ - /// - /// 主键。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long Id { get; init; } - - /// - /// 租户标识。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 状态。 - /// - public TenantVerificationStatus Status { get; init; } - - /// - /// 营业执照号。 - /// - 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; } - - /// - /// 提交时间。 - /// - public DateTime? SubmittedAt { get; init; } - - /// - /// 审核人 ID。 - /// - [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] - public long? ReviewedBy { get; init; } - - /// - /// 审核备注。 - /// - public string? ReviewRemarks { get; init; } - - /// - /// 最新审核人。 - /// - public string? ReviewedByName { get; init; } - - /// - /// 审核时间。 - /// - public DateTime? ReviewedAt { 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 85eb82d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs +++ /dev/null @@ -1,115 +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; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 租户初次绑定订阅处理器。 -/// -public sealed class BindInitialTenantSubscriptionCommandHandler( - ITenantRepository tenantRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - private static readonly ConcurrentDictionary TenantLocks = new(); - - /// - public async Task Handle(BindInitialTenantSubscriptionCommand request, CancellationToken cancellationToken) - { - // 1. 校验请求参数 - if (request.TenantId <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "租户 ID 无效"); - } - - // 1.2 (空行后) 校验套餐 ID - if (request.TenantPackageId <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "套餐 ID 无效"); - } - - // 2. 获取租户级幂等锁,避免并发重复创建 - var tenantLock = GetTenantLock(request.TenantId); - await tenantLock.WaitAsync(cancellationToken); - try - { - // 3. 获取租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 4. 幂等校验:若已存在订阅则直接返回 - var existing = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - if (existing is not null) - { - return existing.ToSubscriptionDto() - ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅读取失败"); - } - - // 5. 创建 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 = "初次绑定订阅" - }; - - // 6. 记录订阅与历史 - 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); - - // 7. 记录审计日志 - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.SubscriptionUpdated, - Title = "初次绑定订阅", - Description = $"套餐 {request.TenantPackageId},时长 0 月" - }, cancellationToken); - - // 8. 保存变更 - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 9. 返回 DTO - return subscription.ToSubscriptionDto() - ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅绑定失败"); - } - finally - { - // 10. 释放幂等锁 - 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/CheckTenantQuotaCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CheckTenantQuotaCommandHandler.cs deleted file mode 100644 index f65ceab..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CheckTenantQuotaCommandHandler.cs +++ /dev/null @@ -1,159 +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 CheckTenantQuotaCommandHandler( - ITenantRepository tenantRepository, - ITenantPackageRepository packageRepository, - ITenantQuotaUsageRepository quotaUsageRepository, - ITenantQuotaUsageHistoryRepository quotaUsageHistoryRepository) - : IRequestHandler -{ - /// - public async Task Handle(CheckTenantQuotaCommand request, CancellationToken cancellationToken) - { - // 1. 校验请求参数 - if (request.TenantId <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "租户 ID 无效"); - } - - // 1.2 (空行后) 校验消耗量 - if (request.Delta <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "配额消耗量必须大于 0"); - } - - // 2. 获取租户与当前订阅 - _ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - if (subscription == null || subscription.EffectiveTo <= DateTime.UtcNow) - { - throw new BusinessException(ErrorCodes.Conflict, "订阅不存在或已到期"); - } - - var package = await packageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在"); - - var limit = ResolveLimit(package, request.QuotaType); - - // 3. 加载配额使用记录并计算 - var usage = await quotaUsageRepository.FindAsync(request.TenantId, request.QuotaType, cancellationToken) - ?? new TenantQuotaUsage - { - TenantId = request.TenantId, - QuotaType = request.QuotaType, - LimitValue = limit ?? 0, - UsedValue = 0, - ResetCycle = ResolveResetCycle(request.QuotaType) - }; - - // 3.1 记录是否为首次初始化(用于落库历史) - var isNewUsage = usage.Id == 0; - - var usedAfter = usage.UsedValue + request.Delta; - if (limit.HasValue && usedAfter > (decimal)limit.Value) - { - usage.LimitValue = limit.Value; - await PersistUsageAsync(usage, quotaUsageRepository, cancellationToken); - throw new BusinessException(ErrorCodes.Conflict, $"{request.QuotaType} 配额不足"); - } - - // 4. 更新使用并保存 - usage.LimitValue = limit ?? usage.LimitValue; - usage.UsedValue = usedAfter; - usage.ResetCycle ??= ResolveResetCycle(request.QuotaType); - - // 4.1 落库历史(初始化 + 本次消耗) - var now = DateTime.UtcNow; - if (isNewUsage) - { - await quotaUsageHistoryRepository.AddAsync(new TenantQuotaUsageHistory - { - TenantId = request.TenantId, - QuotaType = request.QuotaType, - UsedValue = 0, - LimitValue = usage.LimitValue, - RecordedAt = now, - ChangeType = TenantQuotaUsageHistoryChangeType.Init, - ChangeReason = "初始化配额使用记录" - }, cancellationToken); - } - - await quotaUsageHistoryRepository.AddAsync(new TenantQuotaUsageHistory - { - TenantId = request.TenantId, - QuotaType = request.QuotaType, - UsedValue = usage.UsedValue, - LimitValue = usage.LimitValue, - RecordedAt = now, - ChangeType = TenantQuotaUsageHistoryChangeType.Decrease, - ChangeAmount = request.Delta, - ChangeReason = "消耗配额" - }, cancellationToken); - - await PersistUsageAsync(usage, quotaUsageRepository, cancellationToken); - - // 5. 返回结果 - return new QuotaCheckResultDto - { - QuotaType = request.QuotaType, - Limit = limit, - Used = usage.UsedValue, - Remaining = limit.HasValue ? limit.Value - usage.UsedValue : null - }; - } - - private static decimal? ResolveLimit(TenantPackage package, TenantQuotaType quotaType) - { - return quotaType switch - { - TenantQuotaType.StoreCount => package.MaxStoreCount, - TenantQuotaType.AccountCount => package.MaxAccountCount, - TenantQuotaType.Storage => package.MaxStorageGb, - TenantQuotaType.SmsCredits => package.MaxSmsCredits, - TenantQuotaType.DeliveryOrders => package.MaxDeliveryOrders, - _ => null - }; - } - - private static string ResolveResetCycle(TenantQuotaType quotaType) - { - return quotaType switch - { - TenantQuotaType.SmsCredits => "monthly", - TenantQuotaType.DeliveryOrders => "monthly", - _ => "lifetime" - }; - } - - private static async Task PersistUsageAsync( - TenantQuotaUsage usage, - ITenantQuotaUsageRepository quotaUsageRepository, - CancellationToken cancellationToken) - { - // 判断是否为新增。 - if (usage.Id == 0) - { - await quotaUsageRepository.AddAsync(usage, cancellationToken); - } - else - { - await quotaUsageRepository.UpdateAsync(usage, cancellationToken); - } - - await quotaUsageRepository.SaveChangesAsync(cancellationToken); - } -} 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/CreateTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs deleted file mode 100644 index 83ae3a1..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs +++ /dev/null @@ -1,75 +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 CreateTenantAnnouncementCommandHandler( - ITenantAnnouncementRepository announcementRepository, - ICurrentUserAccessor currentUserAccessor) - : IRequestHandler -{ - /// - /// 处理创建租户公告请求。 - /// - /// 创建命令。 - /// 取消标记。 - /// 公告 DTO。 - public async Task Handle(CreateTenantAnnouncementCommand request, CancellationToken cancellationToken) - { - // 1. 校验标题与内容 - if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content)) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "公告标题和内容不能为空"); - } - - if (string.IsNullOrWhiteSpace(request.TargetType)) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "目标受众类型不能为空"); - } - - if (request.EffectiveTo.HasValue && request.EffectiveFrom >= request.EffectiveTo.Value) - { - 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; - var publisherUserId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId; - var announcement = new TenantAnnouncement - { - TenantId = tenantId, - Title = request.Title.Trim(), - Content = request.Content, - AnnouncementType = request.AnnouncementType, - Priority = request.Priority, - EffectiveFrom = request.EffectiveFrom, - EffectiveTo = request.EffectiveTo, - PublisherScope = request.PublisherScope, - PublisherUserId = publisherUserId, - Status = AnnouncementStatus.Draft, - TargetType = request.TargetType.Trim(), - TargetParameters = request.TargetParameters - }; - - // 3. 持久化并返回 DTO - await announcementRepository.AddAsync(announcement, cancellationToken); - await announcementRepository.SaveChangesAsync(cancellationToken); - - return announcement.ToDto(false, null); - } -} 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 e2d9fd5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs +++ /dev/null @@ -1,258 +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.Enums; -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; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 后台手动新增租户处理器。 -/// -public sealed class CreateTenantManuallyCommandHandler( - ITenantRepository tenantRepository, - ITenantPackageRepository tenantPackageRepository, - IIdentityUserRepository identityUserRepository, - IUserRoleRepository userRoleRepository, - IRoleRepository roleRepository, - IPasswordHasher passwordHasher, - IIdGenerator idGenerator, - IMediator mediator, - 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. 创建租户管理员账号(Portal=Tenant) - var adminUser = new IdentityUser - { - Portal = PortalType.Tenant, - 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); - - // 13. 初始化租户管理员角色并绑定到用户(不依赖租户上下文) - await mediator.Send(new CopyRoleTemplateCommand - { - Portal = PortalType.Tenant, - TenantId = tenant.Id, - TemplateCode = "tenant-admin" - }, cancellationToken); - - var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenant.Id, "tenant-admin", cancellationToken); - if (tenantAdminRole != null) - { - await userRoleRepository.ReplaceUserRolesAsync(PortalType.Tenant, tenant.Id, adminUser.Id, new[] { tenantAdminRole.Id }, cancellationToken); - await userRoleRepository.SaveChangesAsync(cancellationToken); - } - - // 14. 回写租户所有者账号 - tenant.PrimaryOwnerUserId = adminUser.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 15. 返回创建结果 - 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 deleted file mode 100644 index ca38d89..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 删除公告处理器。 -/// -public sealed class DeleteTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository) - : IRequestHandler -{ - /// - /// 处理删除公告请求。 - /// - /// 删除命令。 - /// 取消标记。 - /// 执行结果。 - public async Task Handle(DeleteTenantAnnouncementCommand request, CancellationToken cancellationToken) - { - // 1. 删除公告 - await announcementRepository.DeleteAsync(request.TenantId, request.AnnouncementId, cancellationToken); - await announcementRepository.SaveChangesAsync(cancellationToken); - - // 2. 返回执行结果 - 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/GetAnnouncementByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetAnnouncementByIdQueryHandler.cs deleted file mode 100644 index 5a28125..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetAnnouncementByIdQueryHandler.cs +++ /dev/null @@ -1,33 +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 GetAnnouncementByIdQueryHandler( - ITenantAnnouncementRepository announcementRepository) - : IRequestHandler -{ - /// - /// 查询公告详情。 - /// - /// 查询请求。 - /// 取消标记。 - /// 公告 DTO 或 null。 - public async Task Handle(GetAnnouncementByIdQuery request, CancellationToken cancellationToken) - { - // 1. 查询公告主体 - var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken); - if (announcement == null) - { - return null; - } - - // 2. (空行后) 映射 DTO(管理端不返回已读信息) - return announcement.ToDto(false, null); - } -} 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 deleted file mode 100644 index 18a6aae..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs +++ /dev/null @@ -1,28 +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 GetTenantBillQueryHandler(ITenantBillingRepository billingRepository) - : IRequestHandler -{ - /// - /// 查询账单详情。 - /// - /// 查询请求。 - /// 取消标记。 - /// 账单 DTO 或 null。 - public async Task Handle(GetTenantBillQuery request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken); - - // 2. 返回 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/GetTenantPackageByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageByIdQueryHandler.cs deleted file mode 100644 index 4b6b898..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageByIdQueryHandler.cs +++ /dev/null @@ -1,23 +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 GetTenantPackageByIdQueryHandler(ITenantPackageRepository packageRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetTenantPackageByIdQuery request, CancellationToken cancellationToken) - { - // 1. 查询套餐 - var package = await packageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken); - - // 2. 返回 DTO 或 null - return 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/GetTenantQuotaUsageHistoryQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs deleted file mode 100644 index 57ecd65..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs +++ /dev/null @@ -1,171 +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.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Data; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 分页查询租户配额使用历史处理器。 -/// -public sealed class GetTenantQuotaUsageHistoryQueryHandler( - ITenantRepository tenantRepository, - IDapperExecutor dapperExecutor) - : IRequestHandler> -{ - /// - public async Task> Handle(GetTenantQuotaUsageHistoryQuery request, CancellationToken cancellationToken) - { - // 1. 校验租户存在 - _ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2. 规范化分页 - 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. 查询总数 + 列表 - return await dapperExecutor.QueryAsync( - DatabaseConstants.AppDataSource, - DatabaseConnectionRole.Read, - async (connection, token) => - { - // 3.1 统计总数 - var total = await ExecuteScalarIntAsync( - connection, - BuildCountSql(), - [ - ("tenantId", request.TenantId), - ("quotaType", request.QuotaType.HasValue ? (int)request.QuotaType.Value : null), - ("startDate", request.StartDate), - ("endDate", request.EndDate) - ], - token); - - // 3.2 查询列表 - await using var listCommand = CreateCommand( - connection, - BuildListSql(), - [ - ("tenantId", request.TenantId), - ("quotaType", request.QuotaType.HasValue ? (int)request.QuotaType.Value : null), - ("startDate", request.StartDate), - ("endDate", request.EndDate), - ("offset", offset), - ("limit", pageSize) - ]); - - await using var reader = await listCommand.ExecuteReaderAsync(token); - var items = new List(); - while (await reader.ReadAsync(token)) - { - var quotaType = (TenantQuotaType)reader.GetInt32(0); - var usedValue = reader.GetDecimal(1); - var limitValue = reader.GetDecimal(2); - var recordedAt = reader.GetDateTime(3); - var changeType = (TenantQuotaUsageHistoryChangeType)reader.GetInt32(4); - decimal? changeAmount = reader.IsDBNull(5) ? null : reader.GetDecimal(5); - var changeReason = reader.IsDBNull(6) ? null : reader.GetString(6); - - // 3.2.1 映射 DTO - items.Add(new QuotaUsageHistoryDto - { - QuotaType = quotaType, - UsedValue = usedValue, - LimitValue = limitValue, - RecordedAt = recordedAt, - ChangeType = MapChangeType(changeType), - ChangeAmount = changeAmount, - ChangeReason = changeReason - }); - } - - // 3.3 返回分页 - return new PagedResult(items, page, pageSize, total); - }, - cancellationToken); - } - - private static string BuildCountSql() - { - return """ - select count(*) - from public.tenant_quota_usage_histories h - where h."DeletedAt" is null - and h."TenantId" = @tenantId - and (@quotaType::int is null or h."QuotaType" = @quotaType) - and (@startDate::timestamp with time zone is null or h."RecordedAt" >= @startDate) - and (@endDate::timestamp with time zone is null or h."RecordedAt" <= @endDate); - """; - } - - private static string BuildListSql() - { - return """ - select - h."QuotaType", - h."UsedValue", - h."LimitValue", - h."RecordedAt", - h."ChangeType", - h."ChangeAmount", - h."ChangeReason" - from public.tenant_quota_usage_histories h - where h."DeletedAt" is null - and h."TenantId" = @tenantId - and (@quotaType::int is null or h."QuotaType" = @quotaType) - and (@startDate::timestamp with time zone is null or h."RecordedAt" >= @startDate) - and (@endDate::timestamp with time zone is null or h."RecordedAt" <= @endDate) - order by h."RecordedAt" desc, h."Id" desc - offset @offset - limit @limit; - """; - } - - private static string MapChangeType(TenantQuotaUsageHistoryChangeType changeType) - { - return changeType switch - { - TenantQuotaUsageHistoryChangeType.Init => "init", - TenantQuotaUsageHistoryChangeType.Snapshot => "snapshot", - TenantQuotaUsageHistoryChangeType.Increase => "increase", - TenantQuotaUsageHistoryChangeType.Decrease => "decrease", - _ => "snapshot" - }; - } - - 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/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/GetTenantsAnnouncementsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandler.cs deleted file mode 100644 index cd9a74a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandler.cs +++ /dev/null @@ -1,78 +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 GetTenantsAnnouncementsQueryHandler( - ITenantAnnouncementRepository announcementRepository) - : IRequestHandler> -{ - /// - /// 分页查询公告列表。 - /// - /// 查询条件。 - /// 取消标记。 - /// 分页结果。 - public async Task> Handle(GetTenantsAnnouncementsQuery request, CancellationToken cancellationToken) - { - var tenantId = request.TenantId; - var effectiveAt = request.OnlyEffective == true ? DateTime.UtcNow : (DateTime?)null; - - // 计算分页参数 - var page = request.Page <= 0 ? 1 : request.Page; - var size = request.PageSize <= 0 ? 20 : request.PageSize; - - // 估算需要查询的数量:考虑到目标受众过滤可能会移除一些记录, - // 我们查询 3 倍的数量以确保有足够的结果 - var estimatedLimit = page * size * 3; - - // 1. 优化的数据库查询:应用排序和限制 - var announcements = await announcementRepository.SearchAsync( - tenantId, - request.Keyword, - request.Status, - request.AnnouncementType, - request.IsActive, - request.EffectiveFrom, - request.EffectiveTo, - effectiveAt, - orderByPriority: true, // 在数据库端排序 - limit: estimatedLimit, // 限制结果数量 - cancellationToken); - - // 2. 内存过滤:ScheduledPublishAt - if (effectiveAt.HasValue) - { - var at = effectiveAt.Value; - announcements = announcements - .Where(x => x.ScheduledPublishAt == null || x.ScheduledPublishAt <= at) - .ToList(); - } - - // 3. 按租户隔离(仅返回 request.TenantId 对应的公告) - var filtered = announcements - .Where(x => x.TenantId == tenantId) - .ToList(); - - // 4. 分页(数据已在数据库层排序,这里只需 Skip/Take) - var pageItems = filtered - .Skip((page - 1) * size) - .Take(size) - .ToList(); - - // 5. 映射 DTO - var items = pageItems - .Select(a => a.ToDto(false, null)) - .ToList(); - - // 注意:由于我们使用了估算的 limit,总数是 filtered.Count 而不是数据库中的实际总数 - // 这是一个权衡:精确的总数需要额外的 COUNT 查询,代价较高 - return new PagedResult(items, page, size, filtered.Count); - } -} 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 deleted file mode 100644 index 6fd9a8f..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Repositories; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 标记通知已读处理器。 -/// -public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotificationRepository notificationRepository) - : IRequestHandler -{ - /// - /// 标记通知已读。 - /// - /// 标记命令。 - /// 取消标记。 - /// 通知 DTO 或 null。 - public async Task Handle(MarkTenantNotificationReadCommand request, CancellationToken cancellationToken) - { - // 1. 查询通知 - var notification = await notificationRepository.FindByIdAsync(request.TenantId, request.NotificationId, cancellationToken); - if (notification == null) - { - return null; - } - - // 2. 若未读则标记已读 - if (notification.ReadAt == null) - { - notification.ReadAt = DateTime.UtcNow; - await notificationRepository.UpdateAsync(notification, cancellationToken); - await notificationRepository.SaveChangesAsync(cancellationToken); - } - - // 3. 返回 DTO - return notification.ToDto(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs deleted file mode 100644 index 5561031..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs +++ /dev/null @@ -1,82 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.Messaging.Abstractions; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Events; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 发布公告处理器。 -/// -public sealed class PublishAnnouncementCommandHandler( - ITenantAnnouncementRepository announcementRepository, - IEventPublisher eventPublisher) - : IRequestHandler -{ - /// - public async Task Handle(PublishAnnouncementCommand request, CancellationToken cancellationToken) - { - if (request.RowVersion == null || request.RowVersion.Length == 0) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); - } - - // 1. 查询公告 - var tenantId = request.TenantId; - var announcement = await announcementRepository.FindByIdAsync(tenantId, request.AnnouncementId, cancellationToken); - if (announcement == null) - { - return null; - } - - // 2. 校验状态与目标受众 - if (string.IsNullOrWhiteSpace(announcement.TargetType)) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "目标受众类型不能为空"); - } - - if (announcement.Status == AnnouncementStatus.Published) - { - throw new BusinessException(ErrorCodes.Conflict, "公告已发布"); - } - - if (announcement.Status != AnnouncementStatus.Draft && announcement.Status != AnnouncementStatus.Revoked) - { - throw new BusinessException(ErrorCodes.Conflict, "仅草稿或已撤销公告允许发布"); - } - - // 3. 发布公告 - announcement.Status = AnnouncementStatus.Published; - announcement.PublishedAt = DateTime.UtcNow; - announcement.RevokedAt = null; - announcement.RowVersion = request.RowVersion; - - try - { - await announcementRepository.UpdateAsync(announcement, cancellationToken); - await announcementRepository.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) when (exception.GetType().Name == "DbUpdateConcurrencyException") - { - throw new BusinessException(ErrorCodes.Conflict, "公告已被修改,请刷新后重试"); - } - - // 4. 发布领域事件 - await eventPublisher.PublishAsync( - "tenant-announcement.published", - new AnnouncementPublished - { - AnnouncementId = announcement.Id, - PublishedAt = announcement.PublishedAt ?? DateTime.UtcNow, - TargetType = announcement.TargetType - }, - cancellationToken); - - return announcement.ToDto(false, null); - } -} 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/RevokeAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs deleted file mode 100644 index 27c4c46..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs +++ /dev/null @@ -1,75 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.Messaging.Abstractions; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Events; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 撤销公告处理器。 -/// -public sealed class RevokeAnnouncementCommandHandler( - ITenantAnnouncementRepository announcementRepository, - IEventPublisher eventPublisher) - : IRequestHandler -{ - /// - public async Task Handle(RevokeAnnouncementCommand request, CancellationToken cancellationToken) - { - if (request.RowVersion == null || request.RowVersion.Length == 0) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); - } - - // 1. 查询公告 - var tenantId = request.TenantId; - var announcement = await announcementRepository.FindByIdAsync(tenantId, request.AnnouncementId, cancellationToken); - if (announcement == null) - { - return null; - } - - // 2. 校验状态 - if (announcement.Status != AnnouncementStatus.Published) - { - if (announcement.Status == AnnouncementStatus.Revoked) - { - throw new BusinessException(ErrorCodes.Conflict, "公告已撤销"); - } - - throw new BusinessException(ErrorCodes.Conflict, "仅已发布公告允许撤销"); - } - - // 3. 撤销公告 - announcement.Status = AnnouncementStatus.Revoked; - announcement.RevokedAt = DateTime.UtcNow; - announcement.RowVersion = request.RowVersion; - - try - { - await announcementRepository.UpdateAsync(announcement, cancellationToken); - await announcementRepository.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) when (exception.GetType().Name == "DbUpdateConcurrencyException") - { - throw new BusinessException(ErrorCodes.Conflict, "公告已被修改,请刷新后重试"); - } - - // 4. 发布领域事件 - await eventPublisher.PublishAsync( - "tenant-announcement.revoked", - new AnnouncementRevoked - { - AnnouncementId = announcement.Id, - RevokedAt = announcement.RevokedAt ?? DateTime.UtcNow - }, - cancellationToken); - - return announcement.ToDto(false, null); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs deleted file mode 100644 index d20ebf6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs +++ /dev/null @@ -1,35 +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 SearchTenantBillsQueryHandler(ITenantBillingRepository billingRepository) - : IRequestHandler> -{ - /// - /// 分页查询账单列表。 - /// - /// 查询条件。 - /// 取消标记。 - /// 分页结果。 - public async Task> Handle(SearchTenantBillsQuery request, CancellationToken cancellationToken) - { - // 1. 查询账单 - var bills = await billingRepository.SearchAsync(request.TenantId, request.Status, request.From, request.To, cancellationToken); - - // 2. 排序与分页 - 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. 返回分页结果 - 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 deleted file mode 100644 index b74ada5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs +++ /dev/null @@ -1,41 +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 SearchTenantNotificationsQueryHandler(ITenantNotificationRepository notificationRepository) - : IRequestHandler> -{ - /// - /// 分页查询通知列表。 - /// - /// 查询条件。 - /// 取消标记。 - /// 分页结果。 - public async Task> Handle(SearchTenantNotificationsQuery request, CancellationToken cancellationToken) - { - // 1. 查询通知 - var notifications = await notificationRepository.SearchAsync( - request.TenantId, - request.Severity, - request.UnreadOnly, - null, - null, - cancellationToken); - - // 2. 排序与分页 - 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. 返回分页结果 - 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 deleted file mode 100644 index 8771c72..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.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.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 SubmitTenantVerificationCommandHandler( - ITenantRepository tenantRepository, - IIdGenerator idGenerator) - : IRequestHandler -{ - /// - public async Task Handle(SubmitTenantVerificationCommand request, CancellationToken cancellationToken) - { - // 1. 获取租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2. 读取或初始化实名资料 - var profile = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken) - ?? new TenantVerificationProfile { Id = idGenerator.NextId(), TenantId = tenant.Id }; - - // 3. 填充资料 - profile.BusinessLicenseNumber = request.BusinessLicenseNumber; - profile.BusinessLicenseUrl = request.BusinessLicenseUrl; - profile.LegalPersonName = request.LegalPersonName; - profile.LegalPersonIdNumber = request.LegalPersonIdNumber; - profile.LegalPersonIdFrontUrl = request.LegalPersonIdFrontUrl; - profile.LegalPersonIdBackUrl = request.LegalPersonIdBackUrl; - profile.BankAccountName = request.BankAccountName; - profile.BankAccountNumber = request.BankAccountNumber; - profile.BankName = request.BankName; - profile.AdditionalDataJson = request.AdditionalDataJson; - profile.Status = TenantVerificationStatus.Pending; - profile.SubmittedAt = DateTime.UtcNow; - profile.ReviewedAt = null; - profile.ReviewRemarks = null; - profile.ReviewedBy = null; - profile.ReviewedByName = null; - - // 4. 保存资料并记录审计 - await tenantRepository.UpsertVerificationProfileAsync(profile, cancellationToken); - await tenantRepository.AddAuditLogAsync(new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.VerificationSubmitted, - Title = "提交实名认证资料", - Description = request.BusinessLicenseNumber - }, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 5. 返回 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 deleted file mode 100644 index ed19111..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs +++ /dev/null @@ -1,68 +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; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 更新公告处理器。 -/// -public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository) - : IRequestHandler -{ - public async Task Handle(UpdateTenantAnnouncementCommand request, CancellationToken cancellationToken) - { - // 1. 校验输入 - if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content)) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "公告标题和内容不能为空"); - } - - if (request.RowVersion == null || request.RowVersion.Length == 0) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); - } - - // 2. 查询公告 - var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken); - if (announcement == null) - { - return null; - } - - if (announcement.Status != AnnouncementStatus.Draft) - { - if (announcement.Status == AnnouncementStatus.Published) - { - throw new BusinessException(ErrorCodes.Conflict, "已发布公告不可编辑,要编辑已发布公告,请先撤销"); - } - - throw new BusinessException(ErrorCodes.Conflict, "仅草稿公告允许编辑"); - } - - // 3. 更新字段 - 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. 持久化 - try - { - await announcementRepository.UpdateAsync(announcement, cancellationToken); - await announcementRepository.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) when (exception.GetType().Name == "DbUpdateConcurrencyException") - { - throw new BusinessException(ErrorCodes.Conflict, "公告已被修改,请刷新后重试"); - } - - // 5. 返回 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 deleted file mode 100644 index a8c4d94..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs +++ /dev/null @@ -1,72 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 更新租户基础信息处理器。 -/// -public sealed class UpdateTenantCommandHandler( - ITenantRepository tenantRepository, - ILogger logger) - : IRequestHandler -{ - /// - public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken) - { - // 1. 参数校验 - if (request.TenantId <= 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "tenantId 不能为空"); - } - - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new BusinessException(ErrorCodes.BadRequest, "租户名称不能为空"); - } - - // 2. 查询租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 3. 校验租户名称唯一性(排除自身) - var normalizedName = request.Name.Trim(); - if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: request.TenantId, cancellationToken)) - { - throw new BusinessException(ErrorCodes.Conflict, $"租户名称 {normalizedName} 已存在"); - } - - // 4. 校验联系人手机号唯一性(仅当填写时) - if (!string.IsNullOrWhiteSpace(request.ContactPhone)) - { - var normalizedPhone = request.ContactPhone.Trim(); - var existingTenantId = await tenantRepository.FindTenantIdByContactPhoneAsync(normalizedPhone, cancellationToken); - if (existingTenantId.HasValue && existingTenantId.Value != request.TenantId) - { - throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册"); - } - } - - // 5. 更新基础信息(禁止修改 Code) - tenant.Name = normalizedName; - tenant.ShortName = string.IsNullOrWhiteSpace(request.ShortName) ? null : request.ShortName.Trim(); - tenant.Industry = string.IsNullOrWhiteSpace(request.Industry) ? null : request.Industry.Trim(); - tenant.ContactName = string.IsNullOrWhiteSpace(request.ContactName) ? null : request.ContactName.Trim(); - tenant.ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(); - tenant.ContactEmail = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim(); - - // 6. 持久化更新 - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 7. 记录日志 - 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/GetAnnouncementByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetAnnouncementByIdQuery.cs deleted file mode 100644 index 94845a3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetAnnouncementByIdQuery.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 公告详情查询。 -/// -public sealed record GetAnnouncementByIdQuery : IRequest -{ - /// - /// 租户 ID(0 表示平台公告)。 - /// - public long TenantId { get; init; } - - /// - /// 公告 ID。 - /// - public long AnnouncementId { get; init; } -} 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/GetTenantBillQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantBillQuery.cs deleted file mode 100644 index a5ece15..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantBillQuery.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 获取账单详情查询。 -/// -public sealed record GetTenantBillQuery : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 账单 ID。 - /// - public long BillingId { get; init; } -} 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/GetTenantPackageByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageByIdQuery.cs deleted file mode 100644 index 252b5fe..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageByIdQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 获取套餐详情查询。 -/// -public sealed record GetTenantPackageByIdQuery : IRequest -{ - /// - /// 套餐 ID。 - /// - public long TenantPackageId { get; init; } -} 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/GetTenantQuotaUsageHistoryQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantQuotaUsageHistoryQuery.cs deleted file mode 100644 index e49907d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantQuotaUsageHistoryQuery.cs +++ /dev/null @@ -1,43 +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 GetTenantQuotaUsageHistoryQuery : IRequest> -{ - /// - /// 租户 ID。 - /// - public long TenantId { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页大小。 - /// - public int PageSize { get; init; } = 10; - - /// - /// 开始时间(UTC/带时区均可),为空不限制。 - /// - public DateTime? StartDate { get; init; } - - /// - /// 结束时间(UTC/带时区均可),为空不限制。 - /// - public DateTime? EndDate { get; init; } - - /// - /// 配额类型过滤,为空不过滤。 - /// - public TenantQuotaType? QuotaType { 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/GetTenantsAnnouncementsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantsAnnouncementsQuery.cs deleted file mode 100644 index 87f5be5..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantsAnnouncementsQuery.cs +++ /dev/null @@ -1,62 +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 GetTenantsAnnouncementsQuery : IRequest> -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 公告类型筛选。 - /// - public TenantAnnouncementType? AnnouncementType { get; init; } - - /// - /// 公告状态筛选。 - /// - public AnnouncementStatus? Status { get; init; } - - /// - /// 关键词搜索(标题/内容)。 - /// - public string? Keyword { get; init; } - - /// - /// 是否筛选启用状态。 - /// - public bool? IsActive { get; init; } - - /// - /// 生效开始时间筛选(UTC)。 - /// - public DateTime? EffectiveFrom { get; init; } - - /// - /// 生效结束时间筛选(UTC)。 - /// - public DateTime? EffectiveTo { get; init; } - - /// - /// 仅返回当前有效期内的公告。 - /// - public bool? OnlyEffective { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantBillsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantBillsQuery.cs deleted file mode 100644 index 67d0103..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantBillsQuery.cs +++ /dev/null @@ -1,42 +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 SearchTenantBillsQuery : IRequest> -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 账单状态筛选。 - /// - public TenantBillingStatus? Status { get; init; } - - /// - /// 账单起始时间(UTC)筛选。 - /// - public DateTime? From { get; init; } - - /// - /// 账单结束时间(UTC)筛选。 - /// - public DateTime? To { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantNotificationsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantNotificationsQuery.cs deleted file mode 100644 index ff5e8eb..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantNotificationsQuery.cs +++ /dev/null @@ -1,37 +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 SearchTenantNotificationsQuery : IRequest> -{ - /// - /// 租户 ID(雪花算法)。 - /// - public long TenantId { get; init; } - - /// - /// 通知等级筛选。 - /// - public TenantNotificationSeverity? Severity { get; init; } - - /// - /// 仅返回未读通知。 - /// - public bool? UnreadOnly { get; init; } - - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} 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 deleted file mode 100644 index 0616ea9..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs +++ /dev/null @@ -1,221 +0,0 @@ -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants; - -/// -/// 租户 DTO 映射助手。 -/// -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。 - /// - /// 认证实体。 - /// 认证 DTO 或 null。 - public static TenantVerificationDto? ToVerificationDto(this TenantVerificationProfile? profile) - => profile == null - ? null - : new TenantVerificationDto - { - Id = profile.Id, - TenantId = profile.TenantId, - Status = profile.Status, - BusinessLicenseNumber = profile.BusinessLicenseNumber, - BusinessLicenseUrl = profile.BusinessLicenseUrl, - LegalPersonName = profile.LegalPersonName, - LegalPersonIdNumber = profile.LegalPersonIdNumber, - LegalPersonIdFrontUrl = profile.LegalPersonIdFrontUrl, - LegalPersonIdBackUrl = profile.LegalPersonIdBackUrl, - BankAccountName = profile.BankAccountName, - BankAccountNumber = profile.BankAccountNumber, - BankName = profile.BankName, - AdditionalDataJson = profile.AdditionalDataJson, - SubmittedAt = profile.SubmittedAt, - ReviewRemarks = profile.ReviewRemarks, - ReviewedBy = profile.ReviewedBy, - ReviewedByName = profile.ReviewedByName, - 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。 - /// - /// 套餐实体。 - /// 套餐 DTO。 - public static TenantPackageDto ToDto(this TenantPackage package) - => new() - { - Id = package.Id, - Name = package.Name, - Description = package.Description, - PackageType = package.PackageType, - MonthlyPrice = package.MonthlyPrice, - YearlyPrice = package.YearlyPrice, - MaxStoreCount = package.MaxStoreCount, - MaxAccountCount = package.MaxAccountCount, - MaxStorageGb = package.MaxStorageGb, - MaxSmsCredits = package.MaxSmsCredits, - MaxDeliveryOrders = package.MaxDeliveryOrders, - FeaturePoliciesJson = package.FeaturePoliciesJson, - IsActive = package.IsActive, - IsPublicVisible = package.IsPublicVisible, - IsAllowNewTenantPurchase = package.IsAllowNewTenantPurchase, - PublishStatus = package.PublishStatus, - IsRecommended = package.IsRecommended, - Tags = package.Tags ?? [], - SortOrder = package.SortOrder - }; - - /// - /// 将账单实体映射为 DTO。 - /// - /// 账单实体。 - /// 账单 DTO。 - public static TenantBillingDto ToDto(this TenantBillingStatement bill) - => new() - { - Id = bill.Id, - TenantId = bill.TenantId, - StatementNo = bill.StatementNo, - PeriodStart = bill.PeriodStart, - PeriodEnd = bill.PeriodEnd, - AmountDue = bill.AmountDue, - AmountPaid = bill.AmountPaid, - Status = bill.Status, - DueDate = bill.DueDate, - LineItemsJson = bill.LineItemsJson - }; - - /// - /// 将公告实体映射为 DTO。 - /// - /// 公告实体。 - /// 是否已读。 - /// 阅读时间。 - /// 公告 DTO。 - public static TenantAnnouncementDto ToDto(this TenantAnnouncement announcement, bool isRead, DateTime? readAt) - => new() - { - Id = announcement.Id, - TenantId = announcement.TenantId, - Title = announcement.Title, - Content = announcement.Content, - AnnouncementType = announcement.AnnouncementType, - Priority = announcement.Priority, - EffectiveFrom = announcement.EffectiveFrom, - EffectiveTo = announcement.EffectiveTo, - PublisherScope = announcement.PublisherScope, - PublisherUserId = announcement.PublisherUserId, - Status = announcement.Status, - PublishedAt = announcement.PublishedAt, - RevokedAt = announcement.RevokedAt, - ScheduledPublishAt = announcement.ScheduledPublishAt, - TargetType = announcement.TargetType, - TargetParameters = announcement.TargetParameters, - RowVersion = announcement.RowVersion, - IsActive = announcement.Status == AnnouncementStatus.Published, - IsRead = isRead, - ReadAt = readAt - }; - - /// - /// 将通知实体映射为 DTO。 - /// - /// 通知实体。 - /// 通知 DTO。 - public static TenantNotificationDto ToDto(this TenantNotification notification) - => new() - { - Id = notification.Id, - TenantId = notification.TenantId, - Title = notification.Title, - Message = notification.Message, - Channel = notification.Channel, - Severity = notification.Severity, - SentAt = notification.SentAt, - ReadAt = notification.ReadAt, - MetadataJson = notification.MetadataJson - }; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs deleted file mode 100644 index 69e04c4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs +++ /dev/null @@ -1,36 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 创建公告命令验证器。 -/// -public sealed class CreateAnnouncementCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public CreateAnnouncementCommandValidator() - { - RuleFor(x => x.Title) - .NotEmpty() - .MaximumLength(128); - - RuleFor(x => x.Content) - .NotEmpty(); - - RuleFor(x => x.TargetType) - .NotEmpty(); - - RuleFor(x => x) - .Must(x => x.TenantId != 0 || x.PublisherScope == PublisherScope.Platform) - .WithMessage("TenantId=0 仅允许平台公告"); - - RuleFor(x => x.EffectiveFrom) - .LessThan(x => x.EffectiveTo!.Value) - .When(x => x.EffectiveTo.HasValue) - .WithMessage("生效开始时间必须早于结束时间"); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/GetTenantQuotaUsageHistoryQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/GetTenantQuotaUsageHistoryQueryValidator.cs deleted file mode 100644 index 49ea98c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/GetTenantQuotaUsageHistoryQueryValidator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Queries; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 租户配额使用历史查询验证器。 -/// -public sealed class GetTenantQuotaUsageHistoryQueryValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public GetTenantQuotaUsageHistoryQueryValidator() - { - RuleFor(x => x.TenantId).GreaterThan(0); - RuleFor(x => x.Page).GreaterThanOrEqualTo(1); - RuleFor(x => x.PageSize).InclusiveBetween(1, 100); - - // 时间范围校验 - When(x => x.StartDate.HasValue && x.EndDate.HasValue, () => - { - RuleFor(x => x.EndDate!.Value).GreaterThanOrEqualTo(x => x.StartDate!.Value); - }); - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/PublishAnnouncementCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/PublishAnnouncementCommandValidator.cs deleted file mode 100644 index fa32af4..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/PublishAnnouncementCommandValidator.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Commands; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 发布公告命令验证器。 -/// -public sealed class PublishAnnouncementCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public PublishAnnouncementCommandValidator() - { - RuleFor(x => x.AnnouncementId) - .GreaterThan(0); - - RuleFor(x => x.RowVersion) - .NotNull() - .Must(rowVersion => rowVersion != null && rowVersion.Length > 0) - .WithMessage("RowVersion 不能为空"); - } -} 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/RevokeAnnouncementCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/RevokeAnnouncementCommandValidator.cs deleted file mode 100644 index ab4580a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/RevokeAnnouncementCommandValidator.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Commands; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 撤销公告命令验证器。 -/// -public sealed class RevokeAnnouncementCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public RevokeAnnouncementCommandValidator() - { - RuleFor(x => x.AnnouncementId) - .GreaterThan(0); - - RuleFor(x => x.RowVersion) - .NotNull() - .Must(rowVersion => rowVersion != null && rowVersion.Length > 0) - .WithMessage("RowVersion 不能为空"); - } -} 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/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/UpdateAnnouncementCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/UpdateAnnouncementCommandValidator.cs deleted file mode 100644 index f0fa6f2..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/UpdateAnnouncementCommandValidator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Commands; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 更新公告命令验证器。 -/// -public sealed class UpdateAnnouncementCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public UpdateAnnouncementCommandValidator() - { - RuleFor(x => x.Title) - .NotEmpty() - .MaximumLength(128); - - RuleFor(x => x.Content) - .NotEmpty(); - - RuleFor(x => x.RowVersion) - .NotNull() - .Must(rowVersion => rowVersion != null && rowVersion.Length > 0) - .WithMessage("RowVersion 不能为空"); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs index 3caeed2..a40f578 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs @@ -10,12 +10,10 @@ using TakeoutSaaS.Domain.Payments.Repositories; using TakeoutSaaS.Domain.Products.Repositories; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Domain.Tenants.Services; using TakeoutSaaS.Infrastructure.App.Options; using TakeoutSaaS.Infrastructure.App.Persistence; using TakeoutSaaS.Infrastructure.Logs.Persistence; using TakeoutSaaS.Infrastructure.Logs.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence.Repositories; using TakeoutSaaS.Infrastructure.App.Repositories; using TakeoutSaaS.Infrastructure.App.Services; using TakeoutSaaS.Infrastructure.Common.Extensions; @@ -48,22 +46,10 @@ public static class AppServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - // 1. 账单领域/导出服务 - services.AddScoped(); - services.AddScoped(); + // 1. 商户导出服务 services.AddScoped(); // 2. (空行后) 门店配置服务 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs deleted file mode 100644 index aaad29e..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantBillingRepository.cs +++ /dev/null @@ -1,378 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Persistence.Repositories; - -/// -/// 租户账单仓储实现(EF Core)。 -/// -public sealed class TenantBillingRepository(TakeoutAdminDbContext context) : ITenantBillingRepository -{ - /// - public async Task> SearchAsync( - long tenantId, - TenantBillingStatus? status, - DateTime? from, - DateTime? to, - CancellationToken cancellationToken = default) - { - // 1. 构建基础查询:忽略全局过滤器,显式过滤租户与软删除 - var query = context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null && x.TenantId == tenantId); - - // 2. 按状态过滤 - if (status.HasValue) - { - query = query.Where(x => x.Status == status.Value); - } - - // 3. 按日期范围过滤(账单周期) - if (from.HasValue) - { - query = query.Where(x => x.PeriodStart >= from.Value); - } - - if (to.HasValue) - { - query = query.Where(x => x.PeriodEnd <= to.Value); - } - - // 4. 排序返回 - return await query - .OrderByDescending(x => x.PeriodEnd) - .ToListAsync(cancellationToken); - } - - /// - public Task FindByIdAsync(long tenantId, long billingId, CancellationToken cancellationToken = default) - { - return context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.DeletedAt == null && x.TenantId == tenantId && x.Id == billingId, cancellationToken); - } - - /// - public Task FindByStatementNoAsync(long tenantId, string statementNo, CancellationToken cancellationToken = default) - { - var normalized = statementNo.Trim(); - - return context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.DeletedAt == null && x.TenantId == tenantId && x.StatementNo == normalized, cancellationToken); - } - - /// - public Task GetByStatementNoAsync(string statementNo, CancellationToken cancellationToken = default) - { - var normalized = statementNo.Trim(); - - return context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.DeletedAt == null && x.StatementNo == normalized, cancellationToken); - } - - /// - public Task ExistsNotCancelledByPeriodStartAsync( - long tenantId, - DateTime periodStart, - CancellationToken cancellationToken = default) - { - return context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .AnyAsync( - x => x.DeletedAt == null - && x.TenantId == tenantId - && x.PeriodStart == periodStart - && x.Status != TenantBillingStatus.Cancelled, - cancellationToken); - } - - /// - public async Task> GetOverdueBillingsAsync(CancellationToken cancellationToken = default) - { - // 1. 以当前 UTC 时间作为逾期判断基准 - var now = DateTime.UtcNow; - - // 2. 查询逾期且仍处于待支付的账单(仅 Pending 才允许自动切换为 Overdue) - return await context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null - && x.DueDate < now - && x.Status == TenantBillingStatus.Pending) - .OrderBy(x => x.DueDate) - .ToListAsync(cancellationToken); - } - - /// - public async Task> GetBillingsDueSoonAsync(int daysAhead, CancellationToken cancellationToken = default) - { - // 1. 计算到期窗口 - var now = DateTime.UtcNow; - var dueTo = now.AddDays(daysAhead); - - // 2. 仅查询待支付账单 - return await context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null - && x.Status == TenantBillingStatus.Pending - && x.DueDate >= now - && x.DueDate <= dueTo) - .OrderBy(x => x.DueDate) - .ToListAsync(cancellationToken); - } - - /// - public async Task> GetByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default) - { - return await context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null && x.TenantId == tenantId) - .OrderByDescending(x => x.PeriodEnd) - .ToListAsync(cancellationToken); - } - - /// - public async Task> GetByIdsAsync(IReadOnlyCollection billingIds, CancellationToken cancellationToken = default) - { - if (billingIds.Count == 0) - { - return Array.Empty(); - } - - // 1. 忽略全局过滤器以支持管理员端跨租户导出/批量操作 - return await context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null && billingIds.Contains(x.Id)) - .OrderByDescending(x => x.PeriodStart) - .ToListAsync(cancellationToken); - } - - /// - public Task AddAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default) - { - return context.TenantBillingStatements.AddAsync(bill, cancellationToken).AsTask(); - } - - /// - public Task UpdateAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default) - { - context.TenantBillingStatements.Update(bill); - return Task.CompletedTask; - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } - - /// - public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( - long? tenantId, - TenantBillingStatus? status, - DateTime? from, - DateTime? to, - decimal? minAmount, - decimal? maxAmount, - string? keyword, - int pageNumber, - int pageSize, - CancellationToken cancellationToken = default) - { - // 1. 构建基础查询(管理员端跨租户查询,忽略过滤器) - var query = context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null); - - // 2. 按租户过滤(可选) - if (tenantId.HasValue) - { - query = query.Where(x => x.TenantId == tenantId.Value); - } - - // 3. 按状态过滤(可选) - if (status.HasValue) - { - query = query.Where(x => x.Status == status.Value); - } - - // 4. 按日期范围过滤(账单周期) - if (from.HasValue) - { - query = query.Where(x => x.PeriodStart >= from.Value); - } - - if (to.HasValue) - { - query = query.Where(x => x.PeriodEnd <= to.Value); - } - - // 5. 按金额范围过滤(应付金额,包含边界) - if (minAmount.HasValue) - { - query = query.Where(x => x.AmountDue >= minAmount.Value); - } - - if (maxAmount.HasValue) - { - query = query.Where(x => x.AmountDue <= maxAmount.Value); - } - - // 6. 关键字过滤(账单号或租户名) - if (!string.IsNullOrWhiteSpace(keyword)) - { - var normalized = keyword.Trim(); - - query = - from b in query - join t in context.Tenants - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null) - on b.TenantId equals t.Id - where EF.Functions.ILike(b.StatementNo, $"%{normalized}%") - || EF.Functions.ILike(t.Name, $"%{normalized}%") - select b; - } - - // 7. 统计总数 - var total = await query.CountAsync(cancellationToken); - - // 8. 分页查询 - var items = await query - .OrderByDescending(x => x.PeriodEnd) - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize) - .ToListAsync(cancellationToken); - - return (items, total); - } - - /// - public async Task GetStatisticsAsync( - long? tenantId, - DateTime startDate, - DateTime endDate, - string groupBy, - CancellationToken cancellationToken = default) - { - // 1. 构建基础查询(忽略过滤器,显式过滤软删除/租户/时间范围) - var query = context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null - && (!tenantId.HasValue || x.TenantId == tenantId.Value) - && x.PeriodStart >= startDate - && x.PeriodEnd <= endDate); - - // 2. 聚合统计(金额统一使用:应付 - 折扣 + 税费) - var now = DateTime.UtcNow; - var totalAmount = await query.SumAsync(x => x.AmountDue - x.DiscountAmount + x.TaxAmount, cancellationToken); - var paidAmount = await query.Where(x => x.Status == TenantBillingStatus.Paid).SumAsync(x => x.AmountPaid, cancellationToken); - var unpaidAmount = await query.SumAsync(x => (x.AmountDue - x.DiscountAmount + x.TaxAmount) - x.AmountPaid, cancellationToken); - var overdueAmount = await query - .Where(x => (x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue) && x.DueDate < now) - .SumAsync(x => (x.AmountDue - x.DiscountAmount + x.TaxAmount) - x.AmountPaid, cancellationToken); - - // 3. 数量统计 - var totalCount = await query.CountAsync(cancellationToken); - var paidCount = await query.CountAsync(x => x.Status == TenantBillingStatus.Paid, cancellationToken); - var unpaidCount = await query.CountAsync(x => x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue, cancellationToken); - var overdueCount = await query.CountAsync(x => (x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue) && x.DueDate < now, cancellationToken); - - // 4. 趋势统计 - var normalizedGroupBy = NormalizeGroupBy(groupBy); - var trendRaw = await query - .Select(x => new - { - x.PeriodStart, - x.AmountDue, - x.DiscountAmount, - x.TaxAmount, - x.AmountPaid - }) - .ToListAsync(cancellationToken); - - // 4.1 在内存中按 Day/Week/Month 聚合(避免依赖特定数据库函数扩展) - var trend = trendRaw - .GroupBy(x => GetTrendBucket(x.PeriodStart, normalizedGroupBy)) - .Select(g => new TenantBillingTrendDataPoint - { - Period = g.Key, - Count = g.Count(), - TotalAmount = g.Sum(x => x.AmountDue - x.DiscountAmount + x.TaxAmount), - PaidAmount = g.Sum(x => x.AmountPaid) - }) - .OrderBy(x => x.Period) - .ToList(); - - return new TenantBillingStatistics - { - TotalAmount = totalAmount, - PaidAmount = paidAmount, - UnpaidAmount = unpaidAmount, - OverdueAmount = overdueAmount, - TotalCount = totalCount, - PaidCount = paidCount, - UnpaidCount = unpaidCount, - OverdueCount = overdueCount, - TrendData = trend - }; - } - - /// - public Task FindByIdAsync(long billingId, CancellationToken cancellationToken = default) - { - return context.TenantBillingStatements - .IgnoreQueryFilters() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.DeletedAt == null && x.Id == billingId, cancellationToken); - } - - private static string NormalizeGroupBy(string groupBy) - { - return groupBy.Trim() switch - { - "Week" => "Week", - "Month" => "Month", - _ => "Day" - }; - } - - private static DateTime GetTrendBucket(DateTime periodStart, string groupBy) - { - var date = periodStart.Date; - - return groupBy switch - { - "Month" => new DateTime(date.Year, date.Month, 1, 0, 0, 0, DateTimeKind.Utc), - "Week" => GetWeekStart(date), - _ => new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, DateTimeKind.Utc) - }; - } - - private static DateTime GetWeekStart(DateTime date) - { - // 1. 将周一作为一周起始(与 PostgreSQL date_trunc('week', ...) 对齐) - var dayOfWeek = (int)date.DayOfWeek; // Sunday=0, Monday=1, ... - var daysSinceMonday = (dayOfWeek + 6) % 7; - - // 2. 回退到周一 00:00:00(UTC) - var monday = date.AddDays(-daysSinceMonday); - return new DateTime(monday.Year, monday.Month, monday.Day, 0, 0, 0, DateTimeKind.Utc); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantPaymentRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantPaymentRepository.cs deleted file mode 100644 index 0e6a241..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/Repositories/TenantPaymentRepository.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Persistence.Repositories; - -/// -/// 租户支付记录仓储实现(EF Core)。 -/// -public sealed class TenantPaymentRepository(TakeoutAdminDbContext context) : ITenantPaymentRepository -{ - /// - public async Task> GetByBillingIdAsync(long billingStatementId, CancellationToken cancellationToken = default) - { - return await context.TenantPayments - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null && x.BillingStatementId == billingStatementId) - .OrderByDescending(x => x.CreatedAt) - .ToListAsync(cancellationToken); - } - - /// - public async Task GetTotalPaidAmountAsync(long billingStatementId, CancellationToken cancellationToken = default) - { - // 1. 仅统计支付成功的记录 - return await context.TenantPayments - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null - && x.BillingStatementId == billingStatementId - && x.Status == TenantPaymentStatus.Success) - .SumAsync(x => x.Amount, cancellationToken); - } - - /// - public Task FindByIdAsync(long paymentId, CancellationToken cancellationToken = default) - { - return context.TenantPayments - .IgnoreQueryFilters() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.DeletedAt == null && x.Id == paymentId, cancellationToken); - } - - /// - public Task GetByTransactionNoAsync(string transactionNo, CancellationToken cancellationToken = default) - { - var normalized = transactionNo.Trim(); - - return context.TenantPayments - .IgnoreQueryFilters() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.DeletedAt == null && x.TransactionNo == normalized, cancellationToken); - } - - /// - public Task AddAsync(TenantPayment payment, CancellationToken cancellationToken = default) - { - return context.TenantPayments.AddAsync(payment, cancellationToken).AsTask(); - } - - /// - public Task UpdateAsync(TenantPayment payment, CancellationToken cancellationToken = default) - { - context.TenantPayments.Update(payment); - return Task.CompletedTask; - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfQuotaPackageRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfQuotaPackageRepository.cs deleted file mode 100644 index d3226b9..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfQuotaPackageRepository.cs +++ /dev/null @@ -1,169 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// EF 配额包仓储实现。 -/// -public sealed class EfQuotaPackageRepository(TakeoutAdminDbContext context) : IQuotaPackageRepository -{ - #region 配额包定义 - - /// - public Task FindByIdAsync(long id, CancellationToken cancellationToken = default) - { - return context.QuotaPackages - .FirstOrDefaultAsync(x => x.Id == id && x.DeletedAt == null, cancellationToken); - } - - /// - public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( - TenantQuotaType? quotaType, - bool? isActive, - int page, - int pageSize, - CancellationToken cancellationToken = default) - { - var query = context.QuotaPackages.AsNoTracking() - .Where(x => x.DeletedAt == null); - - if (quotaType.HasValue) - { - query = query.Where(x => x.QuotaType == quotaType.Value); - } - - if (isActive.HasValue) - { - query = query.Where(x => x.IsActive == isActive.Value); - } - - var total = await query.CountAsync(cancellationToken); - - var items = await query - .OrderBy(x => x.SortOrder) - .ThenBy(x => x.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(cancellationToken); - - return (items, total); - } - - /// - public Task AddAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default) - { - return context.QuotaPackages.AddAsync(quotaPackage, cancellationToken).AsTask(); - } - - /// - public Task UpdateAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default) - { - context.QuotaPackages.Update(quotaPackage); - return Task.CompletedTask; - } - - /// - public async Task SoftDeleteAsync(long id, CancellationToken cancellationToken = default) - { - var quotaPackage = await context.QuotaPackages - .FirstOrDefaultAsync(x => x.Id == id && x.DeletedAt == null, cancellationToken); - - if (quotaPackage == null) - { - return false; - } - - quotaPackage.DeletedAt = DateTime.UtcNow; - return true; - } - - #endregion - - #region 配额包购买记录 - - /// - public async Task<(IReadOnlyList<(TenantQuotaPackagePurchase Purchase, QuotaPackage Package)> Items, int Total)> GetPurchasesPagedAsync( - long tenantId, - int page, - int pageSize, - CancellationToken cancellationToken = default) - { - var query = context.TenantQuotaPackagePurchases - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.DeletedAt == null); - - var total = await query.CountAsync(cancellationToken); - - var items = await query - .OrderByDescending(x => x.PurchasedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .Join(context.QuotaPackages.AsNoTracking(), - purchase => purchase.QuotaPackageId, - package => package.Id, - (purchase, package) => new { Purchase = purchase, Package = package }) - .ToListAsync(cancellationToken); - - return (items.Select(x => (x.Purchase, x.Package)).ToList(), total); - } - - /// - public Task AddPurchaseAsync(TenantQuotaPackagePurchase purchase, CancellationToken cancellationToken = default) - { - return context.TenantQuotaPackagePurchases.AddAsync(purchase, cancellationToken).AsTask(); - } - - #endregion - - #region 配额使用情况 - - /// - public async Task> GetUsageByTenantAsync( - long tenantId, - TenantQuotaType? quotaType, - CancellationToken cancellationToken = default) - { - var query = context.TenantQuotaUsages - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.TenantId == tenantId); - - if (quotaType.HasValue) - { - query = query.Where(x => x.QuotaType == quotaType.Value); - } - - return await query.ToListAsync(cancellationToken); - } - - /// - public Task FindUsageAsync( - long tenantId, - TenantQuotaType quotaType, - CancellationToken cancellationToken = default) - { - return context.TenantQuotaUsages - .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.QuotaType == quotaType, cancellationToken); - } - - /// - public Task UpdateUsageAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default) - { - context.TenantQuotaUsages.Update(usage); - return Task.CompletedTask; - } - - #endregion - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStatisticsRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStatisticsRepository.cs deleted file mode 100644 index 14fae92..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStatisticsRepository.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// 统计数据仓储实现。 -/// -public sealed class EfStatisticsRepository(TakeoutAdminDbContext dbContext) : IStatisticsRepository -{ - #region 订阅统计 - - /// - public async Task> GetAllSubscriptionsAsync(CancellationToken cancellationToken = default) - { - return await dbContext.TenantSubscriptions - .AsNoTracking() - .ToListAsync(cancellationToken); - } - - /// - public async Task> GetExpiringSubscriptionsAsync( - int daysAhead, - bool onlyWithoutAutoRenew, - CancellationToken cancellationToken = default) - { - var now = DateTime.UtcNow; - var targetDate = now.AddDays(daysAhead); - - // 构建基础查询 - var query = dbContext.TenantSubscriptions - .AsNoTracking() - .Where(s => s.Status == SubscriptionStatus.Active - && s.EffectiveTo >= now - && s.EffectiveTo <= targetDate); - - // 如果只查询未开启自动续费的 - if (onlyWithoutAutoRenew) - { - query = query.Where(s => !s.AutoRenew); - } - - // 连接租户和套餐信息 - var result = await query - .Join( - dbContext.Tenants, - sub => sub.TenantId, - tenant => tenant.Id, - (sub, tenant) => new { Subscription = sub, Tenant = tenant } - ) - .Join( - dbContext.TenantPackages, - combined => combined.Subscription.TenantPackageId, - package => package.Id, - (combined, package) => new ExpiringSubscriptionInfo - { - Subscription = combined.Subscription, - TenantName = combined.Tenant.Name, - PackageName = package.Name - } - ) - .OrderBy(x => x.Subscription.EffectiveTo) - .ToListAsync(cancellationToken); - - return result; - } - - #endregion - - #region 收入统计 - - /// - public async Task> GetPaidBillsAsync(CancellationToken cancellationToken = default) - { - return await dbContext.TenantBillingStatements - .AsNoTracking() - .Where(b => b.Status == TenantBillingStatus.Paid) - .ToListAsync(cancellationToken); - } - - #endregion - - #region 配额使用排行 - - /// - public async Task> GetQuotaUsageRankingAsync( - TenantQuotaType quotaType, - int topN, - CancellationToken cancellationToken = default) - { - return await dbContext.TenantQuotaUsages - .AsNoTracking() - .Where(q => q.QuotaType == quotaType && q.LimitValue > 0) - .Join( - dbContext.Tenants, - quota => quota.TenantId, - tenant => tenant.Id, - (quota, tenant) => new QuotaUsageRankInfo - { - TenantId = quota.TenantId, - TenantName = tenant.Name, - UsedValue = quota.UsedValue, - LimitValue = quota.LimitValue, - UsagePercentage = quota.LimitValue > 0 ? (quota.UsedValue / quota.LimitValue * 100) : 0 - } - ) - .OrderByDescending(x => x.UsagePercentage) - .Take(topN) - .ToListAsync(cancellationToken); - } - - #endregion -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs deleted file mode 100644 index c7c23eb..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs +++ /dev/null @@ -1,412 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; -using TakeoutSaaS.Infrastructure.Logs.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// 订阅管理仓储实现。 -/// -public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, TakeoutLogsDbContext logsContext) : ISubscriptionRepository -{ - #region 订阅查询 - - /// - public async Task FindByIdAsync( - long subscriptionId, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - var query = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - return await query - .FirstOrDefaultAsync(s => s.Id == subscriptionId, cancellationToken); - } - - /// - public async Task> FindByIdsAsync( - IEnumerable subscriptionIds, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - var ids = subscriptionIds.ToList(); - var query = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - return await query - .Where(s => ids.Contains(s.Id)) - .ToListAsync(cancellationToken); - } - - /// - public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( - SubscriptionSearchFilter filter, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - // 1. 构建基础查询 - var subscriptionQuery = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var query = subscriptionQuery - .AsNoTracking() - .Join( - dbContext.Tenants, - sub => sub.TenantId, - tenant => tenant.Id, - (sub, tenant) => new { Subscription = sub, Tenant = tenant } - ) - .Join( - dbContext.TenantPackages, - combined => combined.Subscription.TenantPackageId, - package => package.Id, - (combined, package) => new { combined.Subscription, combined.Tenant, Package = package } - ) - .GroupJoin( - dbContext.TenantPackages, - combined => combined.Subscription.ScheduledPackageId, - scheduledPackage => scheduledPackage.Id, - (combined, scheduledPackages) => new { combined.Subscription, combined.Tenant, combined.Package, ScheduledPackage = scheduledPackages.FirstOrDefault() } - ); - - // 2. 应用过滤条件 - if (filter.Status.HasValue) - { - query = query.Where(x => x.Subscription.Status == filter.Status.Value); - } - - if (filter.TenantPackageId.HasValue) - { - query = query.Where(x => x.Subscription.TenantPackageId == filter.TenantPackageId.Value); - } - - if (filter.TenantId.HasValue) - { - query = query.Where(x => x.Subscription.TenantId == filter.TenantId.Value); - } - - if (!string.IsNullOrWhiteSpace(filter.TenantKeyword)) - { - var keyword = filter.TenantKeyword.Trim().ToLower(); - query = query.Where(x => x.Tenant.Name.ToLower().Contains(keyword) || x.Tenant.Code.ToLower().Contains(keyword)); - } - - if (filter.ExpiringWithinDays.HasValue) - { - var expiryDate = DateTime.UtcNow.AddDays(filter.ExpiringWithinDays.Value); - query = query.Where(x => x.Subscription.EffectiveTo <= expiryDate && x.Subscription.EffectiveTo >= DateTime.UtcNow); - } - - if (filter.AutoRenew.HasValue) - { - query = query.Where(x => x.Subscription.AutoRenew == filter.AutoRenew.Value); - } - - // 3. 获取总数 - var total = await query.CountAsync(cancellationToken); - - // 4. 排序和分页 - var items = await query - .OrderByDescending(x => x.Subscription.CreatedAt) - .Skip((filter.Page - 1) * filter.PageSize) - .Take(filter.PageSize) - .Select(x => new SubscriptionWithRelations - { - Subscription = x.Subscription, - TenantName = x.Tenant.Name, - TenantCode = x.Tenant.Code, - PackageName = x.Package.Name, - ScheduledPackageName = x.ScheduledPackage != null ? x.ScheduledPackage.Name : null - }) - .ToListAsync(cancellationToken); - - return (items, total); - } - - /// - public async Task GetDetailAsync( - long subscriptionId, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - var subscriptionQuery = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var result = await subscriptionQuery - .AsNoTracking() - .Where(s => s.Id == subscriptionId) - .Select(s => new - { - Subscription = s, - Tenant = dbContext.Tenants.FirstOrDefault(t => t.Id == s.TenantId), - Package = dbContext.TenantPackages.FirstOrDefault(p => p.Id == s.TenantPackageId), - ScheduledPackage = s.ScheduledPackageId.HasValue - ? dbContext.TenantPackages.FirstOrDefault(p => p.Id == s.ScheduledPackageId) - : null - }) - .FirstOrDefaultAsync(cancellationToken); - - if (result == null) - { - return null; - } - - return new SubscriptionDetailInfo - { - Subscription = result.Subscription, - TenantName = result.Tenant?.Name ?? "", - TenantCode = result.Tenant?.Code ?? "", - Package = result.Package, - ScheduledPackage = result.ScheduledPackage - }; - } - - /// - public async Task> FindByIdsWithTenantAsync( - IEnumerable subscriptionIds, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - var ids = subscriptionIds.ToList(); - - var query = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - return await query - .Where(s => ids.Contains(s.Id)) - .Join( - dbContext.Tenants, - sub => sub.TenantId, - tenant => tenant.Id, - (sub, tenant) => new SubscriptionWithTenant - { - Subscription = sub, - Tenant = tenant - } - ) - .ToListAsync(cancellationToken); - } - - /// - public async Task> FindAutoRenewalCandidatesAsync( - DateTime now, - DateTime renewalThreshold, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - // 1. 查询开启自动续费且即将到期的活跃订阅 - var subscriptionQuery = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var query = subscriptionQuery - .Where(s => s.Status == SubscriptionStatus.Active - && s.AutoRenew - && s.EffectiveTo <= renewalThreshold - && s.EffectiveTo > now) - .Join( - dbContext.TenantPackages, - subscription => subscription.TenantPackageId, - package => package.Id, - (subscription, package) => new AutoRenewalCandidate - { - Subscription = subscription, - Package = package - }); - - // 2. 返回候选列表 - return await query.ToListAsync(cancellationToken); - } - - /// - public async Task> FindRenewalReminderCandidatesAsync( - DateTime startOfDay, - DateTime endOfDay, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - // 1. 查询到期落在指定区间的订阅(且未开启自动续费) - var subscriptionQuery = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var query = subscriptionQuery - .Where(s => s.Status == SubscriptionStatus.Active - && !s.AutoRenew - && s.EffectiveTo >= startOfDay - && s.EffectiveTo < endOfDay) - .Join( - dbContext.Tenants, - subscription => subscription.TenantId, - tenant => tenant.Id, - (subscription, tenant) => new { Subscription = subscription, Tenant = tenant }) - .Join( - dbContext.TenantPackages, - combined => combined.Subscription.TenantPackageId, - package => package.Id, - (combined, package) => new RenewalReminderCandidate - { - Subscription = combined.Subscription, - Tenant = combined.Tenant, - Package = package - }); - - // 2. 返回候选列表 - return await query.ToListAsync(cancellationToken); - } - - /// - public async Task> FindExpiredActiveSubscriptionsAsync( - DateTime now, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - var query = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - // 1. 查询已到期仍为 Active 的订阅 - return await query - .Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now) - .ToListAsync(cancellationToken); - } - - /// - public async Task> FindGracePeriodExpiredSubscriptionsAsync( - DateTime now, - int gracePeriodDays, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - var query = includeDeleted - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - // 1. 查询宽限期已结束的订阅 - return await query - .Where(s => s.Status == SubscriptionStatus.GracePeriod - && s.EffectiveTo.AddDays(gracePeriodDays) < now) - .ToListAsync(cancellationToken); - } - - #endregion - - #region 套餐查询 - - /// - public async Task FindPackageByIdAsync(long packageId, CancellationToken cancellationToken = default) - { - return await dbContext.TenantPackages - .AsNoTracking() - .FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); - } - - #endregion - - #region 订阅更新 - - /// - public Task UpdateAsync(TenantSubscription subscription, CancellationToken cancellationToken = default) - { - dbContext.TenantSubscriptions.Update(subscription); - return Task.CompletedTask; - } - - #endregion - - #region 订阅历史 - - /// - public Task AddHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default) - { - dbContext.TenantSubscriptionHistories.Add(history); - return Task.CompletedTask; - } - - /// - public async Task> GetHistoryAsync( - long subscriptionId, - CancellationToken cancellationToken = default) - { - return await dbContext.TenantSubscriptionHistories - .AsNoTracking() - .Where(h => h.TenantSubscriptionId == subscriptionId) - .OrderByDescending(h => h.CreatedAt) - .Select(h => new SubscriptionHistoryWithPackageNames - { - History = h, - FromPackageName = dbContext.TenantPackages - .Where(p => p.Id == h.FromPackageId) - .Select(p => p.Name) - .FirstOrDefault() ?? "", - ToPackageName = dbContext.TenantPackages - .Where(p => p.Id == h.ToPackageId) - .Select(p => p.Name) - .FirstOrDefault() ?? "" - }) - .ToListAsync(cancellationToken); - } - - #endregion - - #region 配额使用 - - /// - public async Task> GetQuotaUsagesAsync( - long tenantId, - CancellationToken cancellationToken = default, - bool includeDeleted = false) - { - var query = includeDeleted - ? dbContext.TenantQuotaUsages.IgnoreQueryFilters() - : dbContext.TenantQuotaUsages; - - return await query - .AsNoTracking() - .Where(q => q.TenantId == tenantId) - .ToListAsync(cancellationToken); - } - - #endregion - - #region 通知 - - /// - public Task AddNotificationAsync(TenantNotification notification, CancellationToken cancellationToken = default) - { - dbContext.TenantNotifications.Add(notification); - return Task.CompletedTask; - } - - #endregion - - #region 操作日志 - - /// - public Task AddOperationLogAsync(OperationLog log, CancellationToken cancellationToken = default) - { - logsContext.OperationLogs.Add(log); - return Task.CompletedTask; - } - - #endregion - - /// - public async Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - // 1. 保存业务库变更 - await dbContext.SaveChangesAsync(cancellationToken); - - // 2. 保存日志库变更 - await logsContext.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs deleted file mode 100644 index 98d59ff..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// EF 租户公告仓储。 -/// -public sealed class EfTenantAnnouncementRepository(TakeoutAdminDbContext context) : ITenantAnnouncementRepository -{ - /// - public async Task> SearchAsync( - long tenantId, - string? keyword, - AnnouncementStatus? status, - TenantAnnouncementType? type, - bool? isActive, - DateTime? effectiveFrom, - DateTime? effectiveTo, - DateTime? effectiveAt, - bool orderByPriority = false, - int? limit = null, - CancellationToken cancellationToken = default) - { - var tenantIds = new[] { tenantId, 0L }; - var query = context.TenantAnnouncements.AsNoTracking() - .IgnoreQueryFilters() - .Where(x => tenantIds.Contains(x.TenantId)); - - if (!string.IsNullOrWhiteSpace(keyword)) - { - var normalized = keyword.Trim(); - query = query.Where(x => - EF.Functions.ILike(x.Title, $"%{normalized}%") - || EF.Functions.ILike(x.Content, $"%{normalized}%")); - } - - if (status.HasValue) - { - query = query.Where(x => x.Status == status.Value); - } - - if (type.HasValue) - { - query = query.Where(x => x.AnnouncementType == type.Value); - } - - if (isActive.HasValue) - { - query = isActive.Value - ? query.Where(x => x.Status == AnnouncementStatus.Published) - : query.Where(x => x.Status != AnnouncementStatus.Published); - } - - if (effectiveFrom.HasValue) - { - query = query.Where(x => x.EffectiveFrom >= effectiveFrom.Value); - } - - if (effectiveTo.HasValue) - { - query = query.Where(x => x.EffectiveTo == null || x.EffectiveTo <= effectiveTo.Value); - } - - if (effectiveAt.HasValue) - { - var at = effectiveAt.Value; - query = query.Where(x => x.EffectiveFrom <= at && (x.EffectiveTo == null || x.EffectiveTo >= at)); - } - - // 应用排序(如果启用) - if (orderByPriority) - { - query = query.OrderByDescending(x => x.Priority).ThenByDescending(x => x.EffectiveFrom); - } - - // 应用限制(如果指定) - if (limit.HasValue && limit.Value > 0) - { - query = query.Take(limit.Value); - } - - return await query.ToListAsync(cancellationToken); - } - - /// - public Task FindByIdAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default) - { - return context.TenantAnnouncements.AsNoTracking() - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == announcementId, cancellationToken); - } - - /// - public Task AddAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default) - { - return context.TenantAnnouncements.AddAsync(announcement, cancellationToken).AsTask(); - } - - /// - public Task UpdateAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default) - { - context.TenantAnnouncements.Update(announcement); - return Task.CompletedTask; - } - - /// - public async Task DeleteAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default) - { - var entity = await context.TenantAnnouncements.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == announcementId, cancellationToken); - if (entity != null) - { - context.TenantAnnouncements.Remove(entity); - } - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantNotificationRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantNotificationRepository.cs deleted file mode 100644 index 48794eb..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantNotificationRepository.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// EF 租户通知仓储。 -/// -public sealed class EfTenantNotificationRepository(TakeoutAdminDbContext context) : ITenantNotificationRepository -{ - /// - public Task> SearchAsync( - long tenantId, - TenantNotificationSeverity? severity, - bool? unreadOnly, - DateTime? from, - DateTime? to, - CancellationToken cancellationToken = default) - { - var query = context.TenantNotifications.AsNoTracking() - .Where(x => x.TenantId == tenantId); - - if (severity.HasValue) - { - query = query.Where(x => x.Severity == severity.Value); - } - - if (unreadOnly == true) - { - query = query.Where(x => x.ReadAt == null); - } - - if (from.HasValue) - { - query = query.Where(x => x.SentAt >= from.Value); - } - - if (to.HasValue) - { - query = query.Where(x => x.SentAt <= to.Value); - } - - return query - .OrderByDescending(x => x.SentAt) - .ToListAsync(cancellationToken) - .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); - } - - /// - public Task FindByIdAsync(long tenantId, long notificationId, CancellationToken cancellationToken = default) - { - return context.TenantNotifications - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == notificationId, cancellationToken); - } - - /// - public Task ExistsByMetadataAsync( - long tenantId, - string title, - string metadataJson, - DateTime sentAfter, - CancellationToken cancellationToken = default) - { - return context.TenantNotifications.AsNoTracking() - .AnyAsync( - x => x.TenantId == tenantId - && x.Title == title - && x.MetadataJson == metadataJson - && x.SentAt >= sentAfter, - cancellationToken); - } - - /// - public Task AddAsync(TenantNotification notification, CancellationToken cancellationToken = default) - { - return context.TenantNotifications.AddAsync(notification, cancellationToken).AsTask(); - } - - /// - public Task UpdateAsync(TenantNotification notification, CancellationToken cancellationToken = default) - { - context.TenantNotifications.Update(notification); - return Task.CompletedTask; - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPackageRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPackageRepository.cs deleted file mode 100644 index 2e16c76..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPackageRepository.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// 租户套餐仓储实现。 -/// -public sealed class EfTenantPackageRepository(TakeoutAdminDbContext context) : ITenantPackageRepository -{ - /// - public Task FindByIdAsync(long id, CancellationToken cancellationToken = default) - { - return context.TenantPackages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); - } - - /// - public async Task> SearchAsync(string? keyword, bool? isActive, CancellationToken cancellationToken = default) - { - // 1. 构建基础查询 - var query = context.TenantPackages.AsNoTracking(); - - // 2. 关键字过滤 - if (!string.IsNullOrWhiteSpace(keyword)) - { - var normalized = keyword.Trim(); - query = query.Where(x => EF.Functions.ILike(x.Name, $"%{normalized}%") || EF.Functions.ILike(x.Description ?? string.Empty, $"%{normalized}%")); - } - - // 3. 状态过滤 - if (isActive.HasValue) - { - query = query.Where(x => x.IsActive == isActive.Value); - } - - // 4. 排序返回 - return await query - .OrderBy(x => x.SortOrder) - .ThenByDescending(x => x.CreatedAt) - .ToListAsync(cancellationToken); - } - - /// - public async Task> SearchPublicPurchasableAsync(CancellationToken cancellationToken = default) - { - // 1. 公共可选购套餐仅返回:已发布 + 对外可见 + 允许新购 + 启用 - return await context.TenantPackages.AsNoTracking() - .Where(x => - x.IsActive - && x.PublishStatus == TenantPackagePublishStatus.Published - && x.IsPublicVisible - && x.IsAllowNewTenantPurchase) - .OrderBy(x => x.SortOrder) - .ThenByDescending(x => x.CreatedAt) - .ToListAsync(cancellationToken); - } - - /// - public Task AddAsync(TenantPackage package, CancellationToken cancellationToken = default) - { - return context.TenantPackages.AddAsync(package, cancellationToken).AsTask(); - } - - /// - public Task UpdateAsync(TenantPackage package, CancellationToken cancellationToken = default) - { - context.TenantPackages.Update(package); - return Task.CompletedTask; - } - - /// - public async Task DeleteAsync(long id, CancellationToken cancellationToken = default) - { - var entity = await context.TenantPackages.FirstOrDefaultAsync(x => x.Id == id, cancellationToken); - if (entity != null) - { - context.TenantPackages.Remove(entity); - } - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageHistoryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageHistoryRepository.cs deleted file mode 100644 index 30957bd..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageHistoryRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// 租户配额使用历史仓储实现。 -/// -public sealed class EfTenantQuotaUsageHistoryRepository(TakeoutAdminDbContext context) : ITenantQuotaUsageHistoryRepository -{ - /// - public Task AddAsync(TenantQuotaUsageHistory history, CancellationToken cancellationToken = default) - { - return context.TenantQuotaUsageHistories.AddAsync(history, cancellationToken).AsTask(); - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} - diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageRepository.cs deleted file mode 100644 index b11971e..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantQuotaUsageRepository.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// 租户配额使用仓储实现。 -/// -public sealed class EfTenantQuotaUsageRepository(TakeoutAdminDbContext context) : ITenantQuotaUsageRepository -{ - /// - public Task FindAsync(long tenantId, TenantQuotaType quotaType, CancellationToken cancellationToken = default) - { - return context.TenantQuotaUsages - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.QuotaType == quotaType, cancellationToken); - } - - /// - public Task> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default) - { - return context.TenantQuotaUsages - .AsNoTracking() - .Where(x => x.TenantId == tenantId) - .OrderBy(x => x.QuotaType) - .ToListAsync(cancellationToken) - .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); - } - - /// - public Task AddAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default) - { - return context.TenantQuotaUsages.AddAsync(usage, cancellationToken).AsTask(); - } - - /// - public Task UpdateAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default) - { - context.TenantQuotaUsages.Update(usage); - return Task.CompletedTask; - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingDomainService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingDomainService.cs deleted file mode 100644 index 76da8b6..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingDomainService.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Domain.Tenants.Services; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Infrastructure.App.Services; - -/// -/// 账单领域服务实现。 -/// -public sealed class BillingDomainService( - ITenantBillingRepository billingRepository, - ITenantPackageRepository tenantPackageRepository, - IIdGenerator idGenerator) : IBillingDomainService -{ - /// - public async Task GenerateSubscriptionBillingAsync( - TenantSubscription subscription, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(subscription); - - // 1. 校验幂等:同一周期开始时间只能存在一张未取消账单 - var exists = await billingRepository.ExistsNotCancelledByPeriodStartAsync( - subscription.TenantId, - subscription.EffectiveFrom, - cancellationToken); - if (exists) - { - throw new InvalidOperationException("该订阅周期的账单已存在。"); - } - - // 2. 查询套餐价格信息 - var package = await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken); - if (package is null) - { - throw new InvalidOperationException("订阅未关联有效套餐,无法生成账单。"); - } - - // 3. 选择价格(简化规则:优先按年/按月) - var days = (subscription.EffectiveTo - subscription.EffectiveFrom).TotalDays; - var amountDue = days >= 300 ? package.YearlyPrice : package.MonthlyPrice; - if (!amountDue.HasValue) - { - throw new InvalidOperationException("套餐价格未配置,无法生成账单。"); - } - - // 4. 生成账单明细 - var lineItems = new List - { - BillingLineItem.Create( - itemType: "Subscription", - description: $"套餐 {package.Name} 订阅费用", - quantity: 1, - unitPrice: amountDue.Value) - }; - - // 5. 构建账单实体 - var now = DateTime.UtcNow; - return new TenantBillingStatement - { - Id = idGenerator.NextId(), - TenantId = subscription.TenantId, - StatementNo = GenerateStatementNo(), - BillingType = BillingType.Subscription, - SubscriptionId = subscription.Id, - PeriodStart = subscription.EffectiveFrom, - PeriodEnd = subscription.EffectiveTo, - AmountDue = amountDue.Value, - DiscountAmount = 0m, - TaxAmount = 0m, - AmountPaid = 0m, - Currency = "CNY", - Status = TenantBillingStatus.Pending, - DueDate = now.AddDays(7), - LineItemsJson = JsonSerializer.Serialize(lineItems), - Notes = subscription.Notes - }; - } - - /// - public Task GenerateQuotaPurchaseBillingAsync( - long tenantId, - QuotaPackage quotaPackage, - int quantity, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(quotaPackage); - - if (quantity <= 0) - { - throw new ArgumentOutOfRangeException(nameof(quantity), "购买数量必须大于 0。"); - } - - // 1. 计算金额 - var amountDue = quotaPackage.Price * quantity; - - // 2. 生成账单明细 - var lineItems = new List - { - BillingLineItem.Create( - itemType: "QuotaPurchase", - description: $"配额包 {quotaPackage.Name} × {quantity}", - quantity: quantity, - unitPrice: quotaPackage.Price) - }; - - // 3. 构建账单实体 - var now = DateTime.UtcNow; - var billing = new TenantBillingStatement - { - Id = idGenerator.NextId(), - TenantId = tenantId, - StatementNo = GenerateStatementNo(), - BillingType = BillingType.QuotaPurchase, - SubscriptionId = null, - PeriodStart = now, - PeriodEnd = now, - AmountDue = amountDue, - DiscountAmount = 0m, - TaxAmount = 0m, - AmountPaid = 0m, - Currency = "CNY", - Status = TenantBillingStatus.Pending, - DueDate = now.AddDays(7), - LineItemsJson = JsonSerializer.Serialize(lineItems), - Notes = quotaPackage.Description - }; - - return Task.FromResult(billing); - } - - /// - public string GenerateStatementNo() - { - // 1. 账单号格式:BILL-{yyyyMMdd}-{序号} - var date = DateTime.UtcNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture); - - // 2. 使用雪花 ID 作为全局递增序号,确保分布式唯一 - var sequence = idGenerator.NextId(); - return $"BILL-{date}-{sequence}"; - } - - /// - public async Task ProcessOverdueBillingsAsync(CancellationToken cancellationToken = default) - { - // 1. 查询当前已超过到期日且仍处于待支付的账单(由仓储按 DueDate + Status 筛选) - var overdueBillings = await billingRepository.GetOverdueBillingsAsync(cancellationToken); - if (overdueBillings.Count == 0) - { - return 0; - } - - // 2. 批量标记逾期(防御性:再次判断 Pending) - var processedAt = DateTime.UtcNow; - var updated = 0; - foreach (var billing in overdueBillings) - { - if (billing.Status != TenantBillingStatus.Pending) - { - continue; - } - - billing.MarkAsOverdue(); - billing.OverdueNotifiedAt ??= processedAt; - billing.UpdatedAt = processedAt; - - await billingRepository.UpdateAsync(billing, cancellationToken); - updated++; - } - - // 3. 持久化 - if (updated > 0) - { - await billingRepository.SaveChangesAsync(cancellationToken); - } - - return updated; - } - - /// - public decimal CalculateTotalAmount(decimal baseAmount, decimal discountAmount, decimal taxAmount) - { - return baseAmount - discountAmount + taxAmount; - } - - /// - public bool CanProcessPayment(TenantBillingStatement billing) - { - ArgumentNullException.ThrowIfNull(billing); - - return billing.Status switch - { - TenantBillingStatus.Pending => true, - TenantBillingStatus.Overdue => true, - _ => false - }; - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingExportService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingExportService.cs deleted file mode 100644 index 0fb69e2..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/BillingExportService.cs +++ /dev/null @@ -1,203 +0,0 @@ -using ClosedXML.Excel; -using CsvHelper; -using CsvHelper.Configuration; -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using System.Globalization; -using System.Text; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Services; - -namespace TakeoutSaaS.Infrastructure.App.Services; - -/// -/// 账单导出服务实现(Excel/PDF/CSV)。 -/// -public sealed class BillingExportService : IBillingExportService -{ - /// - /// 初始化导出服务并配置 QuestPDF 许可证。 - /// - public BillingExportService() - { - QuestPDF.Settings.License = LicenseType.Community; - } - - /// - public Task ExportToExcelAsync(IReadOnlyList billings, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(billings); - - // 1. 创建工作簿与工作表 - using var workbook = new XLWorkbook(); - var worksheet = workbook.Worksheets.Add("Billings"); - - // 2. 写入表头 - var headers = new[] - { - "Id", "TenantId", "StatementNo", "BillingType", "Status", - "PeriodStart", "PeriodEnd", "AmountDue", "DiscountAmount", "TaxAmount", "TotalAmount", - "AmountPaid", "Currency", "DueDate", "Notes", "LineItemsJson" - }; - - for (var i = 0; i < headers.Length; i++) - { - worksheet.Cell(1, i + 1).Value = headers[i]; - } - - // 3. 写入数据行 - for (var rowIndex = 0; rowIndex < billings.Count; rowIndex++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var billing = billings[rowIndex]; - var totalAmount = billing.CalculateTotalAmount(); - var r = rowIndex + 2; - - worksheet.Cell(r, 1).Value = billing.Id; - worksheet.Cell(r, 2).Value = billing.TenantId; - worksheet.Cell(r, 3).Value = billing.StatementNo; - worksheet.Cell(r, 4).Value = billing.BillingType.ToString(); - worksheet.Cell(r, 5).Value = billing.Status.ToString(); - worksheet.Cell(r, 6).Value = billing.PeriodStart.ToString("O", CultureInfo.InvariantCulture); - worksheet.Cell(r, 7).Value = billing.PeriodEnd.ToString("O", CultureInfo.InvariantCulture); - worksheet.Cell(r, 8).Value = billing.AmountDue; - worksheet.Cell(r, 9).Value = billing.DiscountAmount; - worksheet.Cell(r, 10).Value = billing.TaxAmount; - worksheet.Cell(r, 11).Value = totalAmount; - worksheet.Cell(r, 12).Value = billing.AmountPaid; - worksheet.Cell(r, 13).Value = billing.Currency; - worksheet.Cell(r, 14).Value = billing.DueDate.ToString("O", CultureInfo.InvariantCulture); - worksheet.Cell(r, 15).Value = billing.Notes ?? string.Empty; - worksheet.Cell(r, 16).Value = billing.LineItemsJson ?? string.Empty; - } - - // 4. 自动调整列宽并输出 - worksheet.Columns().AdjustToContents(); - - using var stream = new MemoryStream(); - workbook.SaveAs(stream); - return Task.FromResult(stream.ToArray()); - } - - /// - public Task ExportToPdfAsync(IReadOnlyList billings, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(billings); - - // 1. 生成 PDF 文档(避免复杂表格,按条目输出) - var document = Document.Create(container => - { - container.Page(page => - { - page.Size(PageSizes.A4); - page.Margin(20); - page.DefaultTextStyle(x => x.FontSize(10)); - - page.Content().Column(column => - { - column.Spacing(6); - - // 2. 标题 - column.Item().Text("Billings Export").FontSize(16).SemiBold(); - - // 3. 逐条输出 - for (var i = 0; i < billings.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var b = billings[i]; - var total = b.CalculateTotalAmount(); - - column.Item().Border(1).BorderColor(Colors.Grey.Lighten2).Padding(8).Column(item => - { - item.Spacing(2); - item.Item().Text($"StatementNo: {b.StatementNo}"); - item.Item().Text($"TenantId: {b.TenantId} BillingType: {b.BillingType} Status: {b.Status}"); - item.Item().Text($"Period: {b.PeriodStart:yyyy-MM-dd} ~ {b.PeriodEnd:yyyy-MM-dd} DueDate: {b.DueDate:yyyy-MM-dd}"); - item.Item().Text($"AmountDue: {b.AmountDue:0.##} Discount: {b.DiscountAmount:0.##} Tax: {b.TaxAmount:0.##}"); - item.Item().Text($"Total: {total:0.##} Paid: {b.AmountPaid:0.##} Currency: {b.Currency}"); - - if (!string.IsNullOrWhiteSpace(b.Notes)) - { - item.Item().Text($"Notes: {b.Notes}"); - } - }); - } - }); - }); - }); - - // 4. 输出字节 - var bytes = document.GeneratePdf(); - return Task.FromResult(bytes); - } - - /// - public async Task ExportToCsvAsync(IReadOnlyList billings, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(billings); - - // 1. 使用 UTF-8 BOM,便于 Excel 直接打开 - await using var stream = new MemoryStream(); - await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), leaveOpen: true); - - var config = new CsvConfiguration(CultureInfo.InvariantCulture) - { - HasHeaderRecord = true - }; - - await using var csv = new CsvWriter(writer, config); - - // 2. 写入表头 - csv.WriteField("Id"); - csv.WriteField("TenantId"); - csv.WriteField("StatementNo"); - csv.WriteField("BillingType"); - csv.WriteField("Status"); - csv.WriteField("PeriodStart"); - csv.WriteField("PeriodEnd"); - csv.WriteField("AmountDue"); - csv.WriteField("DiscountAmount"); - csv.WriteField("TaxAmount"); - csv.WriteField("TotalAmount"); - csv.WriteField("AmountPaid"); - csv.WriteField("Currency"); - csv.WriteField("DueDate"); - csv.WriteField("Notes"); - csv.WriteField("LineItemsJson"); - await csv.NextRecordAsync(); - - // 3. 写入数据行 - foreach (var b in billings) - { - cancellationToken.ThrowIfCancellationRequested(); - - var total = b.CalculateTotalAmount(); - - csv.WriteField(b.Id); - csv.WriteField(b.TenantId); - csv.WriteField(b.StatementNo); - csv.WriteField(b.BillingType.ToString()); - csv.WriteField(b.Status.ToString()); - csv.WriteField(b.PeriodStart.ToString("O", CultureInfo.InvariantCulture)); - csv.WriteField(b.PeriodEnd.ToString("O", CultureInfo.InvariantCulture)); - csv.WriteField(b.AmountDue); - csv.WriteField(b.DiscountAmount); - csv.WriteField(b.TaxAmount); - csv.WriteField(total); - csv.WriteField(b.AmountPaid); - csv.WriteField(b.Currency); - csv.WriteField(b.DueDate.ToString("O", CultureInfo.InvariantCulture)); - csv.WriteField(b.Notes ?? string.Empty); - csv.WriteField(b.LineItemsJson ?? string.Empty); - - await csv.NextRecordAsync(); - } - - // 4. Flush 并返回字节 - await writer.FlushAsync(cancellationToken); - return stream.ToArray(); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/AutoRenewalService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/AutoRenewalService.cs deleted file mode 100644 index 2fa69fe..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/AutoRenewalService.cs +++ /dev/null @@ -1,176 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Infrastructure.App.Persistence; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Infrastructure.BackgroundServices; - -/// -/// 自动续费后台服务。 -/// 定期检查开启自动续费的订阅,在到期前自动生成续费账单。 -/// -public sealed class AutoRenewalService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly AutoRenewalOptions _options; - - public AutoRenewalService( - IServiceProvider serviceProvider, - ILogger logger, - IOptions options) - { - _serviceProvider = serviceProvider; - _logger = logger; - _options = options.Value; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("自动续费服务已启动"); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - // 计算下次执行时间(每天执行) - var now = DateTime.UtcNow; - var nextRun = now.Date.AddDays(1).AddHours(_options.ExecuteHour); - var delay = nextRun - now; - - _logger.LogInformation("自动续费服务将在 {NextRun} 执行,等待 {Delay}", nextRun, delay); - - await Task.Delay(delay, stoppingToken); - - if (stoppingToken.IsCancellationRequested) - break; - - await ProcessAutoRenewalsAsync(stoppingToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "自动续费服务执行异常"); - // 出错后等待一段时间再重试 - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); - } - } - - _logger.LogInformation("自动续费服务已停止"); - } - - private async Task ProcessAutoRenewalsAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("开始处理自动续费"); - - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var idGenerator = scope.ServiceProvider.GetRequiredService(); - - var now = DateTime.UtcNow; - var renewalThreshold = now.AddDays(_options.RenewalDaysBeforeExpiry); - var billsCreated = 0; - - try - { - // 查询开启自动续费且即将到期的活跃订阅 - var autoRenewSubscriptions = await dbContext.TenantSubscriptions - .Where(s => s.Status == SubscriptionStatus.Active - && s.AutoRenew - && s.EffectiveTo <= renewalThreshold - && s.EffectiveTo > now) - .Join( - dbContext.TenantPackages, - sub => sub.TenantPackageId, - package => package.Id, - (sub, package) => new { Subscription = sub, Package = package } - ) - .ToListAsync(cancellationToken); - - foreach (var item in autoRenewSubscriptions) - { - // 检查是否已为本次到期生成过账单 - var existingBill = await dbContext.TenantBillingStatements - .AnyAsync(b => b.TenantId == item.Subscription.TenantId - && b.PeriodStart >= item.Subscription.EffectiveTo - && b.Status != TenantBillingStatus.Cancelled, - cancellationToken); - - if (existingBill) - { - _logger.LogInformation( - "订阅 {SubscriptionId} 已存在续费账单,跳过", - item.Subscription.Id); - continue; - } - - // 生成续费账单 - var billNo = $"BILL-{DateTime.UtcNow:yyyyMMddHHmmss}-{item.Subscription.TenantId}"; - var periodStart = item.Subscription.EffectiveTo; - - // 从当前订阅计算续费周期(月数) - var currentDurationMonths = ((item.Subscription.EffectiveTo.Year - item.Subscription.EffectiveFrom.Year) * 12) - + item.Subscription.EffectiveTo.Month - item.Subscription.EffectiveFrom.Month; - if (currentDurationMonths <= 0) currentDurationMonths = 1; // 至少1个月 - - var periodEnd = periodStart.AddMonths(currentDurationMonths); - - // 根据续费周期计算价格(年付优惠) - var renewalPrice = currentDurationMonths >= 12 - ? (item.Package.YearlyPrice ?? item.Package.MonthlyPrice * 12 ?? 0) - : (item.Package.MonthlyPrice ?? 0) * currentDurationMonths; - - var bill = new TenantBillingStatement - { - Id = idGenerator.NextId(), - TenantId = item.Subscription.TenantId, - StatementNo = billNo, - PeriodStart = periodStart, - PeriodEnd = periodEnd, - AmountDue = renewalPrice, - AmountPaid = 0, - Status = TenantBillingStatus.Pending, - DueDate = periodStart.AddDays(-1), // 到期前一天为付款截止日 - LineItemsJson = $"{{\"套餐名称\":\"{item.Package.Name}\",\"续费周期\":\"{currentDurationMonths}个月\"}}", - CreatedAt = DateTime.UtcNow - }; - - dbContext.TenantBillingStatements.Add(bill); - billsCreated++; - - _logger.LogInformation( - "为订阅 {SubscriptionId} (租户 {TenantId}) 生成自动续费账单 {BillNo},金额 {Amount}", - item.Subscription.Id, item.Subscription.TenantId, billNo, renewalPrice); - } - - await dbContext.SaveChangesAsync(cancellationToken); - - _logger.LogInformation("自动续费处理完成,共生成 {Count} 张账单", billsCreated); - } - catch (Exception ex) - { - _logger.LogError(ex, "自动续费处理失败"); - throw; - } - } -} - -/// -/// 自动续费配置选项。 -/// -public sealed class AutoRenewalOptions -{ - /// - /// 执行时间(小时,UTC时间),默认凌晨1点。 - /// - public int ExecuteHour { get; set; } = 1; - - /// - /// 在到期前N天生成续费账单,默认3天。 - /// - public int RenewalDaysBeforeExpiry { get; set; } = 3; -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/RenewalReminderService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/RenewalReminderService.cs deleted file mode 100644 index a9acd90..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/RenewalReminderService.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Infrastructure.App.Persistence; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Infrastructure.BackgroundServices; - -/// -/// 续费提醒后台服务。 -/// 定期检查即将到期的订阅,发送续费提醒通知。 -/// -public sealed class RenewalReminderService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly RenewalReminderOptions _options; - - public RenewalReminderService( - IServiceProvider serviceProvider, - ILogger logger, - IOptions options) - { - _serviceProvider = serviceProvider; - _logger = logger; - _options = options.Value; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("续费提醒服务已启动"); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - // 计算下次执行时间(每天执行) - var now = DateTime.UtcNow; - var nextRun = now.Date.AddDays(1).AddHours(_options.ExecuteHour); - var delay = nextRun - now; - - _logger.LogInformation("续费提醒服务将在 {NextRun} 执行,等待 {Delay}", nextRun, delay); - - await Task.Delay(delay, stoppingToken); - - if (stoppingToken.IsCancellationRequested) - break; - - await SendRenewalRemindersAsync(stoppingToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "续费提醒服务执行异常"); - // 出错后等待一段时间再重试 - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); - } - } - - _logger.LogInformation("续费提醒服务已停止"); - } - - private async Task SendRenewalRemindersAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("开始发送续费提醒"); - - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var idGenerator = scope.ServiceProvider.GetRequiredService(); - - var now = DateTime.UtcNow; - var remindersSent = 0; - - try - { - // 遍历配置的提醒时间点(例如:到期前7天、3天、1天) - foreach (var daysBeforeExpiry in _options.ReminderDaysBeforeExpiry) - { - var targetDate = now.AddDays(daysBeforeExpiry); - var startOfDay = targetDate.Date; - var endOfDay = startOfDay.AddDays(1); - - // 查询即将到期的活跃订阅(且未开启自动续费) - var expiringSubscriptions = await dbContext.TenantSubscriptions - .Where(s => s.Status == SubscriptionStatus.Active - && !s.AutoRenew - && s.EffectiveTo >= startOfDay - && s.EffectiveTo < endOfDay) - .Join( - dbContext.Tenants, - sub => sub.TenantId, - tenant => tenant.Id, - (sub, tenant) => new { Subscription = sub, Tenant = tenant } - ) - .Join( - dbContext.TenantPackages, - combined => combined.Subscription.TenantPackageId, - package => package.Id, - (combined, package) => new { combined.Subscription, combined.Tenant, Package = package } - ) - .ToListAsync(cancellationToken); - - foreach (var item in expiringSubscriptions) - { - // 检查是否已发送过相同天数的提醒(避免重复发送) - var alreadySent = await dbContext.TenantNotifications - .AnyAsync(n => n.TenantId == item.Subscription.TenantId - && n.Message.Contains($"{daysBeforeExpiry}天内到期") - && n.SentAt >= now.AddHours(-24), // 24小时内已发送过 - cancellationToken); - - if (alreadySent) - { - continue; - } - - // 创建续费提醒通知 - var notification = new TenantNotification - { - Id = idGenerator.NextId(), - TenantId = item.Subscription.TenantId, - Title = "订阅续费提醒", - Message = $"您的订阅套餐「{item.Package.Name}」将在 {daysBeforeExpiry} 天内到期(到期时间:{item.Subscription.EffectiveTo:yyyy-MM-dd HH:mm}),请及时续费以免影响使用。", - Severity = daysBeforeExpiry <= 1 - ? TenantNotificationSeverity.Critical - : TenantNotificationSeverity.Warning, - Channel = TenantNotificationChannel.InApp, - SentAt = DateTime.UtcNow, - ReadAt = null, - CreatedAt = DateTime.UtcNow - }; - - dbContext.TenantNotifications.Add(notification); - remindersSent++; - - _logger.LogInformation( - "发送续费提醒: 租户 {TenantName} ({TenantId}), 套餐 {PackageName}, 剩余 {Days} 天", - item.Tenant.Name, item.Subscription.TenantId, item.Package.Name, daysBeforeExpiry); - } - } - - await dbContext.SaveChangesAsync(cancellationToken); - - _logger.LogInformation("续费提醒发送完成,共发送 {Count} 条提醒", remindersSent); - } - catch (Exception ex) - { - _logger.LogError(ex, "发送续费提醒失败"); - throw; - } - } -} - -/// -/// 续费提醒配置选项。 -/// -public sealed class RenewalReminderOptions -{ - /// - /// 执行时间(小时,UTC时间),默认上午10点。 - /// - public int ExecuteHour { get; set; } = 10; - - /// - /// 提醒时间点(到期前N天),默认7天、3天、1天。 - /// - public int[] ReminderDaysBeforeExpiry { get; set; } = { 7, 3, 1 }; -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs deleted file mode 100644 index 0b62cb0..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.BackgroundServices; - -/// -/// 订阅到期检查后台服务。 -/// 每天凌晨执行,检查即将到期和已到期的订阅,自动更新状态。 -/// -public sealed class SubscriptionExpiryCheckService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly SubscriptionExpiryCheckOptions _options; - - public SubscriptionExpiryCheckService( - IServiceProvider serviceProvider, - ILogger logger, - IOptions options) - { - _serviceProvider = serviceProvider; - _logger = logger; - _options = options.Value; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("订阅到期检查服务已启动"); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - // 计算下次执行时间(每天凌晨) - var now = DateTime.UtcNow; - var nextRun = now.Date.AddDays(1).AddHours(_options.ExecuteHour); - var delay = nextRun - now; - - _logger.LogInformation("订阅到期检查服务将在 {NextRun} 执行,等待 {Delay}", nextRun, delay); - - await Task.Delay(delay, stoppingToken); - - if (stoppingToken.IsCancellationRequested) - break; - - await CheckExpiringSubscriptionsAsync(stoppingToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "订阅到期检查服务执行异常"); - // 出错后等待一段时间再重试 - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); - } - } - - _logger.LogInformation("订阅到期检查服务已停止"); - } - - private async Task CheckExpiringSubscriptionsAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("开始执行订阅到期检查"); - - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var now = DateTime.UtcNow; - var gracePeriodDays = _options.GracePeriodDays; - - try - { - // 1. 检查活跃订阅中已到期的,转为宽限期 - var expiredActive = await dbContext.TenantSubscriptions - .Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now) - .ToListAsync(cancellationToken); - - foreach (var subscription in expiredActive) - { - subscription.Status = SubscriptionStatus.GracePeriod; - _logger.LogInformation( - "订阅 {SubscriptionId} (租户 {TenantId}) 已到期,进入宽限期", - subscription.Id, subscription.TenantId); - } - - // 2. 检查宽限期订阅中超过宽限期的,转为暂停 - var gracePeriodExpired = await dbContext.TenantSubscriptions - .Where(s => s.Status == SubscriptionStatus.GracePeriod - && s.EffectiveTo.AddDays(gracePeriodDays) < now) - .ToListAsync(cancellationToken); - - foreach (var subscription in gracePeriodExpired) - { - subscription.Status = SubscriptionStatus.Suspended; - _logger.LogInformation( - "订阅 {SubscriptionId} (租户 {TenantId}) 宽限期已结束,已暂停", - subscription.Id, subscription.TenantId); - } - - // 3. 保存更改 - var changedCount = await dbContext.SaveChangesAsync(cancellationToken); - - _logger.LogInformation( - "订阅到期检查完成,共更新 {Count} 条记录 (到期转宽限期: {ExpiredCount}, 宽限期转暂停: {SuspendedCount})", - changedCount, expiredActive.Count, gracePeriodExpired.Count); - } - catch (Exception ex) - { - _logger.LogError(ex, "订阅到期检查失败"); - throw; - } - } -} - -/// -/// 订阅到期检查配置选项。 -/// -public sealed class SubscriptionExpiryCheckOptions -{ - /// - /// 执行时间(小时,UTC时间),默认凌晨2点。 - /// - public int ExecuteHour { get; set; } = 2; - - /// - /// 宽限期天数,默认7天。 - /// - public int GracePeriodDays { get; set; } = 7; -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/appsettings.backgroundservices.json b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/appsettings.backgroundservices.json deleted file mode 100644 index 7d4bda2..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/appsettings.backgroundservices.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "BackgroundServices": { - "SubscriptionExpiryCheck": { - "ExecuteHour": 2, - "GracePeriodDays": 7 - }, - "RenewalReminder": { - "ExecuteHour": 10, - "ReminderDaysBeforeExpiry": [7, 3, 1] - }, - "AutoRenewal": { - "ExecuteHour": 1, - "RenewalDaysBeforeExpiry": 3 - } - } -} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs index 337ec40..a7410cc 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs @@ -27,16 +27,6 @@ public static class SchedulerServiceCollectionExtensions .ValidateDataAnnotations() .ValidateOnStart(); - services.AddOptions() - .Bind(configuration.GetSection("Scheduler:SubscriptionAutomation")) - .ValidateDataAnnotations() - .ValidateOnStart(); - - services.AddOptions() - .Bind(configuration.GetSection("Scheduler:BillingAutomation")) - .ValidateDataAnnotations() - .ValidateOnStart(); - services.AddHangfire((serviceProvider, config) => { var options = serviceProvider.GetRequiredService>().CurrentValue; @@ -61,10 +51,8 @@ public static class SchedulerServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/BillingOverdueProcessJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/BillingOverdueProcessJob.cs deleted file mode 100644 index e17a784..0000000 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/BillingOverdueProcessJob.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Application.App.Billings.Commands; -using TakeoutSaaS.Module.Scheduler.Options; - -namespace TakeoutSaaS.Module.Scheduler.Jobs; - -/// -/// 账单逾期标记任务:将超过到期日的待支付账单标记为逾期。 -/// -public sealed class BillingOverdueProcessJob( - IMediator mediator, - IOptionsMonitor optionsMonitor, - ILogger logger) -{ - /// - /// 执行逾期账单标记。 - /// - public async Task ExecuteAsync() - { - // 1. 读取配置并执行逾期处理 - var options = optionsMonitor.CurrentValue; - var updatedCount = await mediator.Send(new ProcessOverdueBillingsCommand()); - - // 2. 记录执行结果 - logger.LogInformation( - "定时任务:逾期账单标记完成,更新 {UpdatedCount} 条(Cron={Cron})", - updatedCount, - options.OverdueBillingProcessCron); - } -} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs deleted file mode 100644 index 648f889..0000000 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Module.Scheduler.Options; - -namespace TakeoutSaaS.Module.Scheduler.Jobs; - -/// -/// 订阅自动续费任务:为即将到期且开启自动续费的订阅生成续费账单。 -/// -public sealed class SubscriptionAutoRenewalJob( - IMediator mediator, - IOptionsMonitor optionsMonitor, - ILogger logger) -{ - /// - /// 执行自动续费账单生成。 - /// - public async Task ExecuteAsync() - { - // 1. 读取配置并执行自动续费 - var options = optionsMonitor.CurrentValue; - var result = await mediator.Send(new ProcessAutoRenewalCommand - { - RenewalDaysBeforeExpiry = options.AutoRenewalDaysBeforeExpiry - }); - - // 2. 记录执行结果 - logger.LogInformation( - "定时任务:自动续费处理完成,候选 {CandidateCount},创建账单 {CreatedBillCount}", - result.CandidateCount, - result.CreatedBillCount); - } -} - diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs deleted file mode 100644 index a6de16a..0000000 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs +++ /dev/null @@ -1,35 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Module.Scheduler.Options; - -namespace TakeoutSaaS.Module.Scheduler.Jobs; - -/// -/// 订阅到期检查任务:到期进入宽限期,宽限期到期自动暂停。 -/// -public sealed class SubscriptionExpiryCheckJob( - IMediator mediator, - IOptionsMonitor optionsMonitor, - ILogger logger) -{ - /// - /// 执行订阅到期检查。 - /// - public async Task ExecuteAsync() - { - // 1. 读取配置并执行到期处理 - var options = optionsMonitor.CurrentValue; - var result = await mediator.Send(new ProcessSubscriptionExpiryCommand - { - GracePeriodDays = options.GracePeriodDays - }); - - // 2. 记录执行结果 - logger.LogInformation( - "定时任务:订阅到期检查完成,进入宽限期 {EnteredGracePeriodCount},暂停 {SuspendedCount}", - result.EnteredGracePeriodCount, - result.SuspendedCount); - } -} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs deleted file mode 100644 index 80b7462..0000000 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs +++ /dev/null @@ -1,35 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Application.App.Subscriptions.Commands; -using TakeoutSaaS.Module.Scheduler.Options; - -namespace TakeoutSaaS.Module.Scheduler.Jobs; - -/// -/// 订阅续费提醒任务:到期前 7/3/1 天发送站内提醒。 -/// -public sealed class SubscriptionRenewalReminderJob( - IMediator mediator, - IOptionsMonitor optionsMonitor, - ILogger logger) -{ - /// - /// 执行续费提醒扫描与发送。 - /// - public async Task ExecuteAsync() - { - // 1. 读取配置并执行续费提醒 - var options = optionsMonitor.CurrentValue; - var result = await mediator.Send(new ProcessRenewalRemindersCommand - { - ReminderDaysBeforeExpiry = options.ReminderDaysBeforeExpiry - }); - - // 2. 记录执行结果 - logger.LogInformation( - "定时任务:续费提醒处理完成,候选 {CandidateCount},创建 {CreatedReminderCount}", - result.CandidateCount, - result.CreatedReminderCount); - } -} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Options/BillingAutomationOptions.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Options/BillingAutomationOptions.cs deleted file mode 100644 index 2ced3bc..0000000 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Options/BillingAutomationOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Module.Scheduler.Options; - -/// -/// 账单自动化相关配置(逾期标记等)。 -/// -public sealed class BillingAutomationOptions -{ - /// - /// 逾期账单标记任务 Cron 表达式(Hangfire)。 - /// 默认每 10 分钟执行一次。 - /// - [Required] - public string OverdueBillingProcessCron { get; set; } = "*/10 * * * *"; -} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Options/SubscriptionAutomationOptions.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Options/SubscriptionAutomationOptions.cs deleted file mode 100644 index 91e1601..0000000 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Options/SubscriptionAutomationOptions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Module.Scheduler.Options; - -/// -/// 订阅自动化相关配置(续费提醒、自动续费、宽限期处理)。 -/// -public sealed class SubscriptionAutomationOptions -{ - /// - /// 自动续费任务执行小时(UTC)。 - /// - [Range(0, 23)] - public int AutoRenewalExecuteHourUtc { get; set; } = 1; - - /// - /// 自动续费:到期前 N 天生成续费账单。 - /// - [Range(0, 365)] - public int AutoRenewalDaysBeforeExpiry { get; set; } = 3; - - /// - /// 续费提醒任务执行小时(UTC)。 - /// - [Range(0, 23)] - public int RenewalReminderExecuteHourUtc { get; set; } = 10; - - /// - /// 续费提醒:到期前 N 天发送提醒。 - /// - [MinLength(1)] - public int[] ReminderDaysBeforeExpiry { get; set; } = [7, 3, 1]; - - /// - /// 订阅到期检查任务执行小时(UTC)。 - /// - [Range(0, 23)] - public int SubscriptionExpiryCheckExecuteHourUtc { get; set; } = 2; - - /// - /// 宽限期天数。 - /// - [Range(0, 365)] - public int GracePeriodDays { get; set; } = 7; -} - diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs index 5d65dc3..6018df3 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs @@ -1,18 +1,13 @@ using Hangfire; -using Microsoft.Extensions.Options; using TakeoutSaaS.Module.Scheduler.Abstractions; using TakeoutSaaS.Module.Scheduler.Jobs; -using TakeoutSaaS.Module.Scheduler.Options; namespace TakeoutSaaS.Module.Scheduler.Services; /// /// 周期性任务注册器。 /// -public sealed class RecurringJobRegistrar( - IOptionsMonitor subscriptionAutomationOptions, - IOptionsMonitor billingAutomationOptions) - : IRecurringJobRegistrar +public sealed class RecurringJobRegistrar : IRecurringJobRegistrar { /// public Task RegisterAsync(CancellationToken cancellationToken = default) @@ -22,29 +17,7 @@ public sealed class RecurringJobRegistrar( RecurringJob.AddOrUpdate("coupons.expire", job => job.ExecuteAsync(), "0 */1 * * *"); RecurringJob.AddOrUpdate("logs.cleanup", job => job.ExecuteAsync(), "0 3 * * *"); - // 2. 订阅自动化任务(自动续费、续费提醒、到期进入宽限期) - var options = subscriptionAutomationOptions.CurrentValue; - RecurringJob.AddOrUpdate( - "subscriptions.auto-renewal", - job => job.ExecuteAsync(), - $"0 {options.AutoRenewalExecuteHourUtc} * * *"); - RecurringJob.AddOrUpdate( - "subscriptions.renewal-reminder", - job => job.ExecuteAsync(), - $"0 {options.RenewalReminderExecuteHourUtc} * * *"); - RecurringJob.AddOrUpdate( - "subscriptions.expiry-check", - job => job.ExecuteAsync(), - $"0 {options.SubscriptionExpiryCheckExecuteHourUtc} * * *"); - - // 3. 账单自动化任务(逾期标记) - var billingOptions = billingAutomationOptions.CurrentValue; - RecurringJob.AddOrUpdate( - "billings.overdue-process", - job => job.ExecuteAsync(), - billingOptions.OverdueBillingProcessCron); - - // 4. (空行后) 门店管理自动化任务 + // 2. (空行后) 门店管理自动化任务 RecurringJob.AddOrUpdate( "stores.business-status-auto-switch", job => job.ExecuteAsync(),