Files
new-api/controller/subscription.go
t0ng7u ecf50b754a 🎨 style: format all code with gofmt and lint:fix
Apply consistent code formatting across the entire codebase using
gofmt and lint:fix tools. This ensures adherence to Go community
standards and improves code readability and maintainability.

Changes include:
- Run gofmt on all .go files to standardize formatting
- Apply lint:fix to automatically resolve linting issues
- Fix code style inconsistencies and formatting violations

No functional changes were made in this commit.
2026-01-30 23:43:27 +08:00

364 lines
9.4 KiB
Go

package controller
import (
"encoding/json"
"errors"
"strconv"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ---- Shared types ----
type SubscriptionPlanDTO struct {
Plan model.SubscriptionPlan `json:"plan"`
Items []model.SubscriptionPlanItem `json:"items"`
}
type BillingPreferenceRequest struct {
BillingPreference string `json:"billing_preference"`
}
func normalizeBillingPreference(pref string) string {
switch strings.TrimSpace(pref) {
case "subscription_first", "wallet_first", "subscription_only", "wallet_only":
return strings.TrimSpace(pref)
default:
return "subscription_first"
}
}
// ---- User APIs ----
func GetSubscriptionPlans(c *gin.Context) {
var plans []model.SubscriptionPlan
if err := model.DB.Where("enabled = ?", true).Order("sort_order desc, id desc").Find(&plans).Error; err != nil {
common.ApiError(c, err)
return
}
result := make([]SubscriptionPlanDTO, 0, len(plans))
for _, p := range plans {
items, _ := model.GetSubscriptionPlanItems(p.Id)
result = append(result, SubscriptionPlanDTO{
Plan: p,
Items: items,
})
}
common.ApiSuccess(c, result)
}
func GetSubscriptionSelf(c *gin.Context) {
userId := c.GetInt("id")
settingMap, _ := model.GetUserSetting(userId, false)
pref := normalizeBillingPreference(settingMap.BillingPreference)
// Get all subscriptions (including expired)
allSubscriptions, err := model.GetAllUserSubscriptions(userId)
if err != nil {
allSubscriptions = []model.SubscriptionSummary{}
}
// Get active subscriptions for backward compatibility
activeSubscriptions, err := model.GetAllActiveUserSubscriptions(userId)
if err != nil {
activeSubscriptions = []model.SubscriptionSummary{}
}
common.ApiSuccess(c, gin.H{
"billing_preference": pref,
"subscriptions": activeSubscriptions, // all active subscriptions
"all_subscriptions": allSubscriptions, // all subscriptions including expired
})
}
func UpdateSubscriptionPreference(c *gin.Context) {
userId := c.GetInt("id")
var req BillingPreferenceRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiErrorMsg(c, "参数错误")
return
}
pref := normalizeBillingPreference(req.BillingPreference)
user, err := model.GetUserById(userId, true)
if err != nil {
common.ApiError(c, err)
return
}
current := user.GetSetting()
current.BillingPreference = pref
user.SetSetting(current)
if err := user.Update(false); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, gin.H{"billing_preference": pref})
}
// ---- Admin APIs ----
func AdminListSubscriptionPlans(c *gin.Context) {
var plans []model.SubscriptionPlan
if err := model.DB.Order("sort_order desc, id desc").Find(&plans).Error; err != nil {
common.ApiError(c, err)
return
}
result := make([]SubscriptionPlanDTO, 0, len(plans))
for _, p := range plans {
items, _ := model.GetSubscriptionPlanItems(p.Id)
result = append(result, SubscriptionPlanDTO{
Plan: p,
Items: items,
})
}
common.ApiSuccess(c, result)
}
type AdminUpsertSubscriptionPlanRequest struct {
Plan model.SubscriptionPlan `json:"plan"`
Items []model.SubscriptionPlanItem `json:"items"`
}
func AdminCreateSubscriptionPlan(c *gin.Context) {
var req AdminUpsertSubscriptionPlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiErrorMsg(c, "参数错误")
return
}
req.Plan.Id = 0
if strings.TrimSpace(req.Plan.Title) == "" {
common.ApiErrorMsg(c, "套餐标题不能为空")
return
}
if req.Plan.Currency == "" {
req.Plan.Currency = "USD"
}
if req.Plan.DurationUnit == "" {
req.Plan.DurationUnit = model.SubscriptionDurationMonth
}
if req.Plan.DurationValue <= 0 && req.Plan.DurationUnit != model.SubscriptionDurationCustom {
req.Plan.DurationValue = 1
}
if len(req.Items) == 0 {
common.ApiErrorMsg(c, "套餐至少需要配置一个模型权益")
return
}
db := model.DB
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&req.Plan).Error; err != nil {
return err
}
items := make([]model.SubscriptionPlanItem, 0, len(req.Items))
for _, it := range req.Items {
if strings.TrimSpace(it.ModelName) == "" {
continue
}
if it.AmountTotal <= 0 {
continue
}
it.Id = 0
it.PlanId = req.Plan.Id
items = append(items, it)
}
if len(items) == 0 {
return errors.New("无有效的模型权益配置")
}
return tx.Create(&items).Error
})
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, req.Plan)
}
func AdminUpdateSubscriptionPlan(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
common.ApiErrorMsg(c, "无效的ID")
return
}
var req AdminUpsertSubscriptionPlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiErrorMsg(c, "参数错误")
return
}
if strings.TrimSpace(req.Plan.Title) == "" {
common.ApiErrorMsg(c, "套餐标题不能为空")
return
}
req.Plan.Id = id
if req.Plan.Currency == "" {
req.Plan.Currency = "USD"
}
if req.Plan.DurationUnit == "" {
req.Plan.DurationUnit = model.SubscriptionDurationMonth
}
if req.Plan.DurationValue <= 0 && req.Plan.DurationUnit != model.SubscriptionDurationCustom {
req.Plan.DurationValue = 1
}
if len(req.Items) == 0 {
common.ApiErrorMsg(c, "套餐至少需要配置一个模型权益")
return
}
err := model.DB.Transaction(func(tx *gorm.DB) error {
// update plan (allow zero values updates with map)
updateMap := map[string]interface{}{
"title": req.Plan.Title,
"subtitle": req.Plan.Subtitle,
"price_amount": req.Plan.PriceAmount,
"currency": req.Plan.Currency,
"duration_unit": req.Plan.DurationUnit,
"duration_value": req.Plan.DurationValue,
"custom_seconds": req.Plan.CustomSeconds,
"enabled": req.Plan.Enabled,
"sort_order": req.Plan.SortOrder,
"stripe_price_id": req.Plan.StripePriceId,
"creem_product_id": req.Plan.CreemProductId,
"updated_at": common.GetTimestamp(),
}
if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil {
return err
}
// replace items
if err := tx.Where("plan_id = ?", id).Delete(&model.SubscriptionPlanItem{}).Error; err != nil {
return err
}
items := make([]model.SubscriptionPlanItem, 0, len(req.Items))
for _, it := range req.Items {
if strings.TrimSpace(it.ModelName) == "" {
continue
}
if it.AmountTotal <= 0 {
continue
}
it.Id = 0
it.PlanId = id
items = append(items, it)
}
if len(items) == 0 {
return errors.New("无有效的模型权益配置")
}
return tx.Create(&items).Error
})
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
func AdminDeleteSubscriptionPlan(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
common.ApiErrorMsg(c, "无效的ID")
return
}
// best practice: disable instead of hard delete to avoid breaking past orders
if err := model.DB.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Update("enabled", false).Error; err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
type AdminBindSubscriptionRequest struct {
UserId int `json:"user_id"`
PlanId int `json:"plan_id"`
}
func AdminBindSubscription(c *gin.Context) {
var req AdminBindSubscriptionRequest
if err := c.ShouldBindJSON(&req); err != nil || req.UserId <= 0 || req.PlanId <= 0 {
common.ApiErrorMsg(c, "参数错误")
return
}
if err := model.AdminBindSubscription(req.UserId, req.PlanId, ""); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
// ---- Admin: user subscription management ----
func AdminListUserSubscriptions(c *gin.Context) {
userId, _ := strconv.Atoi(c.Param("id"))
if userId <= 0 {
common.ApiErrorMsg(c, "无效的用户ID")
return
}
subs, err := model.AdminListUserSubscriptions(userId)
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, subs)
}
type AdminCreateUserSubscriptionRequest struct {
PlanId int `json:"plan_id"`
}
// AdminCreateUserSubscription creates a new user subscription from a plan (no payment).
func AdminCreateUserSubscription(c *gin.Context) {
userId, _ := strconv.Atoi(c.Param("id"))
if userId <= 0 {
common.ApiErrorMsg(c, "无效的用户ID")
return
}
var req AdminCreateUserSubscriptionRequest
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
common.ApiErrorMsg(c, "参数错误")
return
}
if err := model.AdminBindSubscription(userId, req.PlanId, ""); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
// AdminInvalidateUserSubscription cancels a user subscription immediately.
func AdminInvalidateUserSubscription(c *gin.Context) {
subId, _ := strconv.Atoi(c.Param("id"))
if subId <= 0 {
common.ApiErrorMsg(c, "无效的订阅ID")
return
}
if err := model.AdminInvalidateUserSubscription(subId); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
// AdminDeleteUserSubscription hard-deletes a user subscription.
func AdminDeleteUserSubscription(c *gin.Context) {
subId, _ := strconv.Atoi(c.Param("id"))
if subId <= 0 {
common.ApiErrorMsg(c, "无效的订阅ID")
return
}
if err := model.AdminDeleteUserSubscription(subId); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
// ---- Helper: serialize provider payload safely ----
func jsonString(v any) string {
b, _ := json.Marshal(v)
return string(b)
}