billing.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. package service
  2. import (
  3. "fmt"
  4. "net/http"
  5. "strings"
  6. "github.com/QuantumNous/new-api/common"
  7. "github.com/QuantumNous/new-api/logger"
  8. "github.com/QuantumNous/new-api/model"
  9. relaycommon "github.com/QuantumNous/new-api/relay/common"
  10. "github.com/QuantumNous/new-api/types"
  11. "github.com/gin-gonic/gin"
  12. )
  13. const (
  14. BillingSourceWallet = "wallet"
  15. BillingSourceSubscription = "subscription"
  16. )
  17. // PreConsumeBilling decides whether to pre-consume from subscription or wallet based on user preference.
  18. // It also always pre-consumes token quota in quota units (same as legacy flow).
  19. func PreConsumeBilling(c *gin.Context, preConsumedQuota int, relayInfo *relaycommon.RelayInfo) *types.NewAPIError {
  20. if relayInfo == nil {
  21. return types.NewError(fmt.Errorf("relayInfo is nil"), types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
  22. }
  23. pref := common.NormalizeBillingPreference(relayInfo.UserSetting.BillingPreference)
  24. trySubscription := func() *types.NewAPIError {
  25. quotaType := 0
  26. // For total quota: consume preConsumedQuota quota units.
  27. subConsume := int64(preConsumedQuota)
  28. if subConsume <= 0 {
  29. subConsume = 1
  30. }
  31. // Pre-consume token quota in quota units to keep token limits consistent.
  32. if preConsumedQuota > 0 {
  33. if err := PreConsumeTokenQuota(relayInfo, preConsumedQuota); err != nil {
  34. return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
  35. }
  36. }
  37. res, err := model.PreConsumeUserSubscription(relayInfo.RequestId, relayInfo.UserId, relayInfo.OriginModelName, quotaType, subConsume)
  38. if err != nil {
  39. // revert token pre-consume when subscription fails
  40. if preConsumedQuota > 0 && !relayInfo.IsPlayground {
  41. _ = model.IncreaseTokenQuota(relayInfo.TokenId, relayInfo.TokenKey, preConsumedQuota)
  42. }
  43. errMsg := err.Error()
  44. if strings.Contains(errMsg, "no active subscription") || strings.Contains(errMsg, "subscription quota insufficient") {
  45. return types.NewErrorWithStatusCode(fmt.Errorf("订阅额度不足或未配置订阅: %s", errMsg), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
  46. }
  47. return types.NewErrorWithStatusCode(fmt.Errorf("订阅预扣失败: %s", errMsg), types.ErrorCodeQueryDataError, http.StatusInternalServerError)
  48. }
  49. relayInfo.BillingSource = BillingSourceSubscription
  50. relayInfo.SubscriptionId = res.UserSubscriptionId
  51. relayInfo.SubscriptionPreConsumed = res.PreConsumed
  52. relayInfo.SubscriptionPostDelta = 0
  53. relayInfo.SubscriptionAmountTotal = res.AmountTotal
  54. relayInfo.SubscriptionAmountUsedAfterPreConsume = res.AmountUsedAfter
  55. if planInfo, err := model.GetSubscriptionPlanInfoByUserSubscriptionId(res.UserSubscriptionId); err == nil && planInfo != nil {
  56. relayInfo.SubscriptionPlanId = planInfo.PlanId
  57. relayInfo.SubscriptionPlanTitle = planInfo.PlanTitle
  58. }
  59. relayInfo.FinalPreConsumedQuota = preConsumedQuota
  60. logger.LogInfo(c, fmt.Sprintf("用户 %d 使用订阅计费预扣:订阅=%d,token_quota=%d", relayInfo.UserId, res.PreConsumed, preConsumedQuota))
  61. return nil
  62. }
  63. tryWallet := func() *types.NewAPIError {
  64. relayInfo.BillingSource = BillingSourceWallet
  65. relayInfo.SubscriptionId = 0
  66. relayInfo.SubscriptionPreConsumed = 0
  67. return PreConsumeQuota(c, preConsumedQuota, relayInfo)
  68. }
  69. switch pref {
  70. case "subscription_only":
  71. return trySubscription()
  72. case "wallet_only":
  73. return tryWallet()
  74. case "wallet_first":
  75. if err := tryWallet(); err != nil {
  76. // only fallback for insufficient wallet quota
  77. if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
  78. return trySubscription()
  79. }
  80. return err
  81. }
  82. return nil
  83. case "subscription_first":
  84. fallthrough
  85. default:
  86. if err := trySubscription(); err != nil {
  87. // fallback only when subscription not available/insufficient
  88. if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
  89. return tryWallet()
  90. }
  91. return err
  92. }
  93. return nil
  94. }
  95. }