mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-06-07 22:09:57 +00:00
✨ 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:
@@ -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
|
||||
}
|
||||
|
||||
// 验证订单类型,目前只处理一次性付款(充值)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user