mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 09:35:21 +00:00
Improve subscription payment safety and data integrity by handling user/URL lookup failures, fixing Stripe subscription mode, persisting quota reset fields, and correcting subscription delta accounting and DB timestamp casting. Refine the UI with stricter custom duration validation, accurate currency rounding, conditional Epay labeling, rollback on preference update failure, and shared subscription formatting helpers plus clearer component naming.
130 lines
3.2 KiB
Go
130 lines
3.2 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/thanhpk/randstr"
|
|
)
|
|
|
|
type SubscriptionCreemPayRequest struct {
|
|
PlanId int `json:"plan_id"`
|
|
}
|
|
|
|
func SubscriptionRequestCreemPay(c *gin.Context) {
|
|
var req SubscriptionCreemPayRequest
|
|
|
|
// Keep body for debugging consistency (like RequestCreemPay)
|
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
log.Printf("read subscription creem pay req body err: %v", err)
|
|
c.JSON(200, gin.H{"message": "error", "data": "read query error"})
|
|
return
|
|
}
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
|
|
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
|
|
return
|
|
}
|
|
|
|
plan, err := model.GetSubscriptionPlanById(req.PlanId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if !plan.Enabled {
|
|
common.ApiErrorMsg(c, "套餐未启用")
|
|
return
|
|
}
|
|
if plan.CreemProductId == "" {
|
|
common.ApiErrorMsg(c, "该套餐未配置 CreemProductId")
|
|
return
|
|
}
|
|
if setting.CreemWebhookSecret == "" && !setting.CreemTestMode {
|
|
common.ApiErrorMsg(c, "Creem Webhook 未配置")
|
|
return
|
|
}
|
|
|
|
userId := c.GetInt("id")
|
|
user, err := model.GetUserById(userId, false)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if user == nil {
|
|
common.ApiErrorMsg(c, "用户不存在")
|
|
return
|
|
}
|
|
|
|
if plan.MaxPurchasePerUser > 0 {
|
|
count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if count >= int64(plan.MaxPurchasePerUser) {
|
|
common.ApiErrorMsg(c, "已达到该套餐购买上限")
|
|
return
|
|
}
|
|
}
|
|
|
|
reference := "sub-creem-ref-" + randstr.String(6)
|
|
referenceId := "sub_ref_" + common.Sha1([]byte(reference+time.Now().String()+user.Username))
|
|
|
|
// create pending order first
|
|
order := &model.SubscriptionOrder{
|
|
UserId: userId,
|
|
PlanId: plan.Id,
|
|
Money: plan.PriceAmount,
|
|
TradeNo: referenceId,
|
|
PaymentMethod: PaymentMethodCreem,
|
|
CreateTime: time.Now().Unix(),
|
|
Status: common.TopUpStatusPending,
|
|
}
|
|
if err := order.Insert(); err != nil {
|
|
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
|
|
return
|
|
}
|
|
|
|
// Reuse Creem checkout generator by building a lightweight product reference.
|
|
currency := "USD"
|
|
switch operation_setting.GetGeneralSetting().QuotaDisplayType {
|
|
case operation_setting.QuotaDisplayTypeCNY:
|
|
currency = "CNY"
|
|
case operation_setting.QuotaDisplayTypeUSD:
|
|
currency = "USD"
|
|
default:
|
|
currency = "USD"
|
|
}
|
|
product := &CreemProduct{
|
|
ProductId: plan.CreemProductId,
|
|
Name: plan.Title,
|
|
Price: plan.PriceAmount,
|
|
Currency: currency,
|
|
Quota: 0,
|
|
}
|
|
|
|
checkoutUrl, err := genCreemLink(referenceId, product, user.Email, user.Username)
|
|
if err != nil {
|
|
log.Printf("获取Creem支付链接失败: %v", err)
|
|
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
|
return
|
|
}
|
|
|
|
c.JSON(200, gin.H{
|
|
"message": "success",
|
|
"data": gin.H{
|
|
"checkout_url": checkoutUrl,
|
|
"order_id": referenceId,
|
|
},
|
|
})
|
|
}
|