Files
new-api/service/billing.go
t0ng7u 2297af731c 🔧 chore: Unify subscription plan status toggle with PATCH endpoint
Replace separate enable/disable flows with a single PATCH API that updates the enabled flag.
Update frontend hooks and table actions to call the unified endpoint and keep UI behavior consistent.
Introduce a minimal admin controller handler and route for the status update.
2026-01-31 14:27:01 +08:00

109 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"fmt"
"net/http"
"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 {
quotaTypes := model.GetModelQuotaTypes(relayInfo.OriginModelName)
quotaType := 0
if len(quotaTypes) > 0 {
quotaType = quotaTypes[0]
}
// For subscription item: per-request consumes 1, per-quota consumes preConsumedQuota quota units.
subConsume := int64(preConsumedQuota)
if quotaType == 1 {
subConsume = 1
}
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)
}
return types.NewErrorWithStatusCode(fmt.Errorf("订阅额度不足或未配置订阅: %s", err.Error()), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
}
relayInfo.BillingSource = BillingSourceSubscription
relayInfo.SubscriptionItemId = res.ItemId
relayInfo.SubscriptionQuotaType = quotaType
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 使用订阅计费预扣:订阅=%dtoken_quota=%d", relayInfo.UserId, res.PreConsumed, preConsumedQuota))
return nil
}
tryWallet := func() *types.NewAPIError {
relayInfo.BillingSource = BillingSourceWallet
relayInfo.SubscriptionItemId = 0
relayInfo.SubscriptionQuotaType = 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
return tryWallet()
}
return nil
}
}