🚀 refactor: Simplify subscription quota to total amount model

Remove per-model subscription items and switch to a single total quota per plan and user subscription. Update billing, reset, and logging flows to operate on total quota, and refactor admin/user UI to configure and display total quota consistently.
This commit is contained in:
t0ng7u
2026-02-01 00:35:08 +08:00
parent b92a4ee987
commit 6300c31d70
17 changed files with 270 additions and 999 deletions

View File

@@ -26,17 +26,9 @@ func PreConsumeBilling(c *gin.Context, preConsumedQuota int, relayInfo *relaycom
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.
// For total quota: consume preConsumedQuota quota units.
subConsume := int64(preConsumedQuota)
if quotaType == 1 {
subConsume = 1
}
if subConsume <= 0 {
subConsume = 1
}
@@ -58,8 +50,7 @@ func PreConsumeBilling(c *gin.Context, preConsumedQuota int, relayInfo *relaycom
}
relayInfo.BillingSource = BillingSourceSubscription
relayInfo.SubscriptionItemId = res.ItemId
relayInfo.SubscriptionQuotaType = quotaType
relayInfo.SubscriptionId = res.UserSubscriptionId
relayInfo.SubscriptionPreConsumed = res.PreConsumed
relayInfo.SubscriptionPostDelta = 0
relayInfo.SubscriptionAmountTotal = res.AmountTotal
@@ -76,8 +67,7 @@ func PreConsumeBilling(c *gin.Context, preConsumedQuota int, relayInfo *relaycom
tryWallet := func() *types.NewAPIError {
relayInfo.BillingSource = BillingSourceWallet
relayInfo.SubscriptionItemId = 0
relayInfo.SubscriptionQuotaType = 0
relayInfo.SubscriptionId = 0
relayInfo.SubscriptionPreConsumed = 0
return PreConsumeQuota(c, preConsumedQuota, relayInfo)
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/types"
@@ -90,15 +89,14 @@ func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interf
other["billing_preference"] = relayInfo.UserSetting.BillingPreference
}
if relayInfo.BillingSource == "subscription" {
if relayInfo.SubscriptionItemId != 0 {
other["subscription_item_id"] = relayInfo.SubscriptionItemId
if relayInfo.SubscriptionId != 0 {
other["subscription_id"] = relayInfo.SubscriptionId
}
other["subscription_quota_type"] = relayInfo.SubscriptionQuotaType
if relayInfo.SubscriptionPreConsumed > 0 {
other["subscription_pre_consumed"] = relayInfo.SubscriptionPreConsumed
}
// post_delta: settlement delta applied after actual usage is known (can be negative for refund)
if relayInfo.SubscriptionQuotaType == 0 && relayInfo.SubscriptionPostDelta != 0 {
if relayInfo.SubscriptionPostDelta != 0 {
other["subscription_post_delta"] = relayInfo.SubscriptionPostDelta
}
if relayInfo.SubscriptionPlanId != 0 {
@@ -108,12 +106,8 @@ func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interf
other["subscription_plan_title"] = relayInfo.SubscriptionPlanTitle
}
// Compute "this request" subscription consumed + remaining
consumed := relayInfo.SubscriptionPreConsumed
usedFinal := relayInfo.SubscriptionAmountUsedAfterPreConsume
if relayInfo.SubscriptionQuotaType == 0 {
consumed = relayInfo.SubscriptionPreConsumed + relayInfo.SubscriptionPostDelta
usedFinal = relayInfo.SubscriptionAmountUsedAfterPreConsume + relayInfo.SubscriptionPostDelta
}
consumed := relayInfo.SubscriptionPreConsumed + relayInfo.SubscriptionPostDelta
usedFinal := relayInfo.SubscriptionAmountUsedAfterPreConsume + relayInfo.SubscriptionPostDelta
if consumed < 0 {
consumed = 0
}
@@ -132,13 +126,6 @@ func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interf
if consumed > 0 {
other["subscription_consumed"] = consumed
}
// Fallback: if plan info missing (older requests), best-effort fetch by item id.
if relayInfo.SubscriptionPlanId == 0 && relayInfo.SubscriptionItemId != 0 {
if info, err := model.GetSubscriptionPlanInfoBySubscriptionItemId(relayInfo.SubscriptionItemId); err == nil && info != nil {
other["subscription_plan_id"] = info.PlanId
other["subscription_plan_title"] = info.PlanTitle
}
}
// Wallet quota is not deducted when billed from subscription.
other["wallet_quota_deducted"] = 0
}

View File

@@ -17,7 +17,7 @@ import (
func ReturnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo) {
// Always refund subscription pre-consumed (can be non-zero even when FinalPreConsumedQuota is 0)
needRefundSub := relayInfo.BillingSource == BillingSourceSubscription && relayInfo.SubscriptionItemId != 0 && relayInfo.SubscriptionPreConsumed > 0
needRefundSub := relayInfo.BillingSource == BillingSourceSubscription && relayInfo.SubscriptionId != 0 && relayInfo.SubscriptionPreConsumed > 0
needRefundToken := relayInfo.FinalPreConsumedQuota != 0
if !needRefundSub && !needRefundToken {
return

View File

@@ -505,16 +505,15 @@ func PostConsumeQuota(relayInfo *relaycommon.RelayInfo, quota int, preConsumedQu
// 1) Consume from wallet quota OR subscription item
if relayInfo != nil && relayInfo.BillingSource == BillingSourceSubscription {
// For subscription: quotaType=0 uses quota units delta; quotaType=1 uses fixed 0 delta (pre-consumed 1 on request begin)
if relayInfo.SubscriptionQuotaType == 0 {
if relayInfo.SubscriptionItemId == 0 {
return errors.New("subscription item id is missing")
}
if err := model.PostConsumeUserSubscriptionDelta(relayInfo.SubscriptionItemId, int64(quota)); err != nil {
if relayInfo.SubscriptionId == 0 {
return errors.New("subscription id is missing")
}
delta := int64(quota) - relayInfo.SubscriptionPreConsumed
if delta != 0 {
if err := model.PostConsumeUserSubscriptionDelta(relayInfo.SubscriptionId, delta); err != nil {
return err
}
// Track delta for logging/UI (net consumed = preConsumed + postDelta)
relayInfo.SubscriptionPostDelta += int64(quota)
relayInfo.SubscriptionPostDelta += delta
}
} else {
// Wallet

View File

@@ -53,7 +53,7 @@ func runSubscriptionQuotaResetOnce() {
ctx := context.Background()
totalReset := 0
for {
n, err := model.ResetDueSubscriptionItems(subscriptionResetBatchSize)
n, err := model.ResetDueSubscriptions(subscriptionResetBatchSize)
if err != nil {
logger.LogWarn(ctx, fmt.Sprintf("subscription quota reset task failed: %v", err))
return