This commit is contained in:
2026-04-25 20:36:28 +08:00
parent 18c92584d9
commit e89459f093
3 changed files with 96 additions and 0 deletions

View File

@@ -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. 检查用户是否已订阅该产品

View File

@@ -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)),

View File

@@ -11,6 +11,8 @@ import (
const (
// QuotaLedgerChangeTypePurchaseForSub 主账号为子账号购买额度
QuotaLedgerChangeTypePurchaseForSub = "purchase_for_sub"
// QuotaLedgerChangeTypeConsumeAPI 用户调用API消耗额度
QuotaLedgerChangeTypeConsumeAPI = "api_consume"
)
// SubordinateQuotaPurchase 主账号为子账号购买额度记录