diff --git a/internal/application/api/api_application_service.go b/internal/application/api/api_application_service.go index 04ad165..b9172a1 100644 --- a/internal/application/api/api_application_service.go +++ b/internal/application/api/api_application_service.go @@ -20,6 +20,8 @@ import ( finance_services "tyapi-server/internal/domains/finance/services" product_entities "tyapi-server/internal/domains/product/entities" product_services "tyapi-server/internal/domains/product/services" + subordinate_entities "tyapi-server/internal/domains/subordinate/entities" + subordinate_repositories "tyapi-server/internal/domains/subordinate/repositories" user_repositories "tyapi-server/internal/domains/user/repositories" task_entities "tyapi-server/internal/infrastructure/task/entities" "tyapi-server/internal/infrastructure/task/interfaces" @@ -93,6 +95,7 @@ type ApiApplicationServiceImpl struct { walletService finance_services.WalletAggregateService subscriptionService *product_services.ProductSubscriptionService balanceAlertService finance_services.BalanceAlertService + subordinateRepo subordinate_repositories.SubordinateRepository } func NewApiApplicationService( @@ -112,6 +115,7 @@ func NewApiApplicationService( subscriptionService *product_services.ProductSubscriptionService, exportManager *export.ExportManager, balanceAlertService finance_services.BalanceAlertService, + subordinateRepo subordinate_repositories.SubordinateRepository, ) ApiApplicationService { service := &ApiApplicationServiceImpl{ apiCallService: apiCallService, @@ -130,6 +134,7 @@ func NewApiApplicationService( walletService: walletService, subscriptionService: subscriptionService, balanceAlertService: balanceAlertService, + subordinateRepo: subordinateRepo, } return service @@ -1108,6 +1113,24 @@ func (s *ApiApplicationServiceImpl) ProcessDeduction(ctx context.Context, cmd *c return err } + // 优先扣减产品额度(若存在且可用),避免子账号有额度却因钱包余额不足失败 + deductedByQuota, err := s.tryDeductQuota(ctx, cmd.UserID, cmd.ProductID, cmd.ApiCallID, cmd.TransactionID) + if err != nil { + s.logger.Error("额度扣减失败", + zap.String("transaction_id", cmd.TransactionID), + zap.String("user_id", cmd.UserID), + zap.String("product_id", cmd.ProductID), + zap.Error(err)) + return err + } + if deductedByQuota { + s.logger.Info("额度扣减成功", + zap.String("transaction_id", cmd.TransactionID), + zap.String("user_id", cmd.UserID), + zap.String("product_id", cmd.ProductID)) + return nil + } + if err := s.walletService.Deduct(ctx, cmd.UserID, amount, cmd.ApiCallID, cmd.TransactionID, cmd.ProductID); err != nil { s.logger.Error("扣款处理失败", zap.String("transaction_id", cmd.TransactionID), @@ -1202,6 +1225,25 @@ func (s *ApiApplicationServiceImpl) ProcessCompensation(ctx context.Context, cmd // validateWalletStatus 验证钱包状态 func (s *ApiApplicationServiceImpl) validateWalletStatus(ctx context.Context, userID string, product *product_entities.Product, subscription *product_entities.Subscription) error { + // 若用户在该产品有可用额度,则本次调用将走额度扣减,不再要求钱包余额预检通过 + if s.subordinateRepo != nil { + quotaAccount, err := s.subordinateRepo.FindQuotaAccount(ctx, userID, product.ID) + if err != nil { + s.logger.Error("查询额度账户失败", + zap.String("user_id", userID), + zap.String("product_id", product.ID), + zap.Error(err)) + return ErrSystem + } + if quotaAccount != nil && quotaAccount.AvailableQuota > 0 { + s.logger.Info("额度校验通过,跳过钱包余额预检", + zap.String("user_id", userID), + zap.String("product_id", product.ID), + zap.Int64("available_quota", quotaAccount.AvailableQuota)) + return nil + } + } + // 1. 获取用户钱包信息 wallet, err := s.walletService.LoadWalletByUserId(ctx, userID) if err != nil { @@ -1251,6 +1293,56 @@ func (s *ApiApplicationServiceImpl) validateWalletStatus(ctx context.Context, us return nil } +// tryDeductQuota 尝试扣减产品额度;若不存在额度账户则返回 false,nil 以便回退钱包扣款 +func (s *ApiApplicationServiceImpl) tryDeductQuota(ctx context.Context, userID, productID, apiCallID, transactionID string) (bool, error) { + if s.subordinateRepo == nil || productID == "" { + return false, nil + } + + var deducted bool + err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { + account, err := s.subordinateRepo.FindQuotaAccount(txCtx, userID, productID) + if err != nil { + return err + } + if account == nil { + return nil + } + if account.AvailableQuota <= 0 { + return ErrInsufficientBalance + } + + before := account.AvailableQuota + account.AvailableQuota -= 1 + account.UsedQuota += 1 + if err := s.subordinateRepo.UpdateQuotaAccount(txCtx, account); err != nil { + return err + } + + ledger := &subordinate_entities.UserProductQuotaLedger{ + UserID: userID, + ProductID: productID, + ChangeType: subordinate_entities.QuotaLedgerChangeTypeConsumeAPI, + DeltaQuota: -1, + BeforeQuota: before, + AfterQuota: account.AvailableQuota, + SourceID: apiCallID, + OperatorID: userID, + Remark: fmt.Sprintf("API调用扣减,transaction_id=%s", transactionID), + } + if err := s.subordinateRepo.CreateQuotaLedger(txCtx, ledger); err != nil { + return err + } + + deducted = true + return nil + }) + if err != nil { + return false, err + } + return deducted, nil +} + // validateSubscriptionStatus 验证订阅状态并返回订阅信息 func (s *ApiApplicationServiceImpl) validateSubscriptionStatus(ctx context.Context, userID string, product *product_entities.Product) (*product_entities.Subscription, error) { // 1. 检查用户是否已订阅该产品 diff --git a/internal/container/container.go b/internal/container/container.go index 8a2372a..9a48ebd 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -811,6 +811,7 @@ func NewContainer() *Container { subscriptionService *product_services.ProductSubscriptionService, exportManager *export.ExportManager, balanceAlertService finance_services.BalanceAlertService, + subordinateRepo domain_subordinate_repo.SubordinateRepository, ) api_app.ApiApplicationService { return api_app.NewApiApplicationService( apiCallService, @@ -829,6 +830,7 @@ func NewContainer() *Container { subscriptionService, exportManager, balanceAlertService, + subordinateRepo, ) }, fx.As(new(api_app.ApiApplicationService)), diff --git a/internal/domains/subordinate/entities/quota.go b/internal/domains/subordinate/entities/quota.go index fea8ab5..2d713b7 100644 --- a/internal/domains/subordinate/entities/quota.go +++ b/internal/domains/subordinate/entities/quota.go @@ -11,6 +11,8 @@ import ( const ( // QuotaLedgerChangeTypePurchaseForSub 主账号为子账号购买额度 QuotaLedgerChangeTypePurchaseForSub = "purchase_for_sub" + // QuotaLedgerChangeTypeConsumeAPI 用户调用API消耗额度 + QuotaLedgerChangeTypeConsumeAPI = "api_consume" ) // SubordinateQuotaPurchase 主账号为子账号购买额度记录