mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 11:51:20 +00:00
Add missing strings import for subscription fallback checks, log failed subscription refunds after retries, and extend sidebar module settings with a subscription management toggle plus translations.
107 lines
4.0 KiB
Go
107 lines
4.0 KiB
Go
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/QuantumNous/new-api/common"
|
||
"github.com/QuantumNous/new-api/logger"
|
||
"github.com/QuantumNous/new-api/model"
|
||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||
"github.com/QuantumNous/new-api/types"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
const (
|
||
BillingSourceWallet = "wallet"
|
||
BillingSourceSubscription = "subscription"
|
||
)
|
||
|
||
// PreConsumeBilling decides whether to pre-consume from subscription or wallet based on user preference.
|
||
// It also always pre-consumes token quota in quota units (same as legacy flow).
|
||
func PreConsumeBilling(c *gin.Context, preConsumedQuota int, relayInfo *relaycommon.RelayInfo) *types.NewAPIError {
|
||
if relayInfo == nil {
|
||
return types.NewError(fmt.Errorf("relayInfo is nil"), types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
|
||
}
|
||
|
||
pref := common.NormalizeBillingPreference(relayInfo.UserSetting.BillingPreference)
|
||
trySubscription := func() *types.NewAPIError {
|
||
quotaType := 0
|
||
// For total quota: consume preConsumedQuota quota units.
|
||
subConsume := int64(preConsumedQuota)
|
||
if subConsume <= 0 {
|
||
subConsume = 1
|
||
}
|
||
|
||
// Pre-consume token quota in quota units to keep token limits consistent.
|
||
if preConsumedQuota > 0 {
|
||
if err := PreConsumeTokenQuota(relayInfo, preConsumedQuota); err != nil {
|
||
return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
|
||
}
|
||
}
|
||
|
||
res, err := model.PreConsumeUserSubscription(relayInfo.RequestId, relayInfo.UserId, relayInfo.OriginModelName, quotaType, subConsume)
|
||
if err != nil {
|
||
// revert token pre-consume when subscription fails
|
||
if preConsumedQuota > 0 && !relayInfo.IsPlayground {
|
||
_ = model.IncreaseTokenQuota(relayInfo.TokenId, relayInfo.TokenKey, preConsumedQuota)
|
||
}
|
||
errMsg := err.Error()
|
||
if strings.Contains(errMsg, "no active subscription") || strings.Contains(errMsg, "subscription quota insufficient") {
|
||
return types.NewErrorWithStatusCode(fmt.Errorf("订阅额度不足或未配置订阅: %s", errMsg), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
|
||
}
|
||
return types.NewErrorWithStatusCode(fmt.Errorf("订阅预扣失败: %s", errMsg), types.ErrorCodeQueryDataError, http.StatusInternalServerError)
|
||
}
|
||
|
||
relayInfo.BillingSource = BillingSourceSubscription
|
||
relayInfo.SubscriptionId = res.UserSubscriptionId
|
||
relayInfo.SubscriptionPreConsumed = res.PreConsumed
|
||
relayInfo.SubscriptionPostDelta = 0
|
||
relayInfo.SubscriptionAmountTotal = res.AmountTotal
|
||
relayInfo.SubscriptionAmountUsedAfterPreConsume = res.AmountUsedAfter
|
||
if planInfo, err := model.GetSubscriptionPlanInfoByUserSubscriptionId(res.UserSubscriptionId); err == nil && planInfo != nil {
|
||
relayInfo.SubscriptionPlanId = planInfo.PlanId
|
||
relayInfo.SubscriptionPlanTitle = planInfo.PlanTitle
|
||
}
|
||
relayInfo.FinalPreConsumedQuota = preConsumedQuota
|
||
|
||
logger.LogInfo(c, fmt.Sprintf("用户 %d 使用订阅计费预扣:订阅=%d,token_quota=%d", relayInfo.UserId, res.PreConsumed, preConsumedQuota))
|
||
return nil
|
||
}
|
||
|
||
tryWallet := func() *types.NewAPIError {
|
||
relayInfo.BillingSource = BillingSourceWallet
|
||
relayInfo.SubscriptionId = 0
|
||
relayInfo.SubscriptionPreConsumed = 0
|
||
return PreConsumeQuota(c, preConsumedQuota, relayInfo)
|
||
}
|
||
|
||
switch pref {
|
||
case "subscription_only":
|
||
return trySubscription()
|
||
case "wallet_only":
|
||
return tryWallet()
|
||
case "wallet_first":
|
||
if err := tryWallet(); err != nil {
|
||
// only fallback for insufficient wallet quota
|
||
if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
|
||
return trySubscription()
|
||
}
|
||
return err
|
||
}
|
||
return nil
|
||
case "subscription_first":
|
||
fallthrough
|
||
default:
|
||
if err := trySubscription(); err != nil {
|
||
// fallback only when subscription not available/insufficient
|
||
if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
|
||
return tryWallet()
|
||
}
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
}
|