feat(subscription): harden subscription billing with resets, idempotency, and production-grade stability

Add plan-level quota reset periods and display/reset cadence in admin/UI
Enforce natural reset alignment with background reset task and cleanup job
Make subscription pre-consume/refund idempotent with request-scoped records and retries
Use database time for consistent resets across multi-instance deployments
Harden payment callbacks with locking and idempotent order completion
Record subscription purchases in topup history and billing logs
Optimize subscription queries and add critical composite indexes
This commit is contained in:
t0ng7u
2026-01-31 00:31:47 +08:00
parent 5707ee3492
commit ffebb35499
10 changed files with 350 additions and 59 deletions

View File

@@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
@@ -298,15 +299,16 @@ func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) {
return
}
// Subscription order takes precedence (accept both onetime/subscription types)
if model.GetSubscriptionOrderByTradeNo(referenceId) != nil {
if err := model.CompleteSubscriptionOrder(referenceId, jsonString(event)); err != nil {
log.Printf("Creem订阅订单处理失败: %s, 订单号: %s", err.Error(), referenceId)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
// Try complete subscription order first
LockOrder(referenceId)
defer UnlockOrder(referenceId)
if err := model.CompleteSubscriptionOrder(referenceId, jsonString(event)); err == nil {
c.Status(http.StatusOK)
return
} else if err != nil && !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
log.Printf("Creem订阅订单处理失败: %s, 订单号: %s", err.Error(), referenceId)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
// 验证订单类型,目前只处理一次性付款(充值)

View File

@@ -1,6 +1,7 @@
package controller
import (
"errors"
"fmt"
"io"
"log"
@@ -166,17 +167,19 @@ func sessionCompleted(event stripe.Event) {
return
}
// Subscription order takes precedence
if model.GetSubscriptionOrderByTradeNo(referenceId) != nil {
payload := map[string]any{
"customer": customerId,
"amount_total": event.GetObjectValue("amount_total"),
"currency": strings.ToUpper(event.GetObjectValue("currency")),
"event_type": string(event.Type),
}
if err := model.CompleteSubscriptionOrder(referenceId, jsonString(payload)); err != nil {
log.Println("complete subscription order failed:", err.Error(), referenceId)
}
// Try complete subscription order first
LockOrder(referenceId)
defer UnlockOrder(referenceId)
payload := map[string]any{
"customer": customerId,
"amount_total": event.GetObjectValue("amount_total"),
"currency": strings.ToUpper(event.GetObjectValue("currency")),
"event_type": string(event.Type),
}
if err := model.CompleteSubscriptionOrder(referenceId, jsonString(payload)); err == nil {
return
} else if err != nil && !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
log.Println("complete subscription order failed:", err.Error(), referenceId)
return
}
@@ -205,10 +208,12 @@ func sessionExpired(event stripe.Event) {
}
// Subscription order expiration
if model.GetSubscriptionOrderByTradeNo(referenceId) != nil {
if err := model.ExpireSubscriptionOrder(referenceId); err != nil {
log.Println("过期订阅订单失败", referenceId, ", err:", err.Error())
}
LockOrder(referenceId)
defer UnlockOrder(referenceId)
if err := model.ExpireSubscriptionOrder(referenceId); err == nil {
return
} else if err != nil && !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
log.Println("过期订阅订单失败", referenceId, ", err:", err.Error())
return
}