subscription.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  1. package model
  2. import (
  3. "errors"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/QuantumNous/new-api/common"
  10. "github.com/QuantumNous/new-api/pkg/cachex"
  11. "github.com/samber/hot"
  12. "gorm.io/gorm"
  13. )
  14. // Subscription duration units
  15. const (
  16. SubscriptionDurationYear = "year"
  17. SubscriptionDurationMonth = "month"
  18. SubscriptionDurationDay = "day"
  19. SubscriptionDurationHour = "hour"
  20. SubscriptionDurationCustom = "custom"
  21. )
  22. // Subscription quota reset period
  23. const (
  24. SubscriptionResetNever = "never"
  25. SubscriptionResetDaily = "daily"
  26. SubscriptionResetWeekly = "weekly"
  27. SubscriptionResetMonthly = "monthly"
  28. SubscriptionResetCustom = "custom"
  29. )
  30. var (
  31. ErrSubscriptionOrderNotFound = errors.New("subscription order not found")
  32. ErrSubscriptionOrderStatusInvalid = errors.New("subscription order status invalid")
  33. )
  34. const (
  35. subscriptionPlanCacheNamespace = "new-api:subscription_plan:v1"
  36. subscriptionPlanInfoCacheNamespace = "new-api:subscription_plan_info:v1"
  37. )
  38. var (
  39. subscriptionPlanCacheOnce sync.Once
  40. subscriptionPlanInfoCacheOnce sync.Once
  41. subscriptionPlanCache *cachex.HybridCache[SubscriptionPlan]
  42. subscriptionPlanInfoCache *cachex.HybridCache[SubscriptionPlanInfo]
  43. )
  44. func subscriptionPlanCacheTTL() time.Duration {
  45. ttlSeconds := common.GetEnvOrDefault("SUBSCRIPTION_PLAN_CACHE_TTL", 300)
  46. if ttlSeconds <= 0 {
  47. ttlSeconds = 300
  48. }
  49. return time.Duration(ttlSeconds) * time.Second
  50. }
  51. func subscriptionPlanInfoCacheTTL() time.Duration {
  52. ttlSeconds := common.GetEnvOrDefault("SUBSCRIPTION_PLAN_INFO_CACHE_TTL", 120)
  53. if ttlSeconds <= 0 {
  54. ttlSeconds = 120
  55. }
  56. return time.Duration(ttlSeconds) * time.Second
  57. }
  58. func subscriptionPlanCacheCapacity() int {
  59. capacity := common.GetEnvOrDefault("SUBSCRIPTION_PLAN_CACHE_CAP", 5000)
  60. if capacity <= 0 {
  61. capacity = 5000
  62. }
  63. return capacity
  64. }
  65. func subscriptionPlanInfoCacheCapacity() int {
  66. capacity := common.GetEnvOrDefault("SUBSCRIPTION_PLAN_INFO_CACHE_CAP", 10000)
  67. if capacity <= 0 {
  68. capacity = 10000
  69. }
  70. return capacity
  71. }
  72. func getSubscriptionPlanCache() *cachex.HybridCache[SubscriptionPlan] {
  73. subscriptionPlanCacheOnce.Do(func() {
  74. ttl := subscriptionPlanCacheTTL()
  75. subscriptionPlanCache = cachex.NewHybridCache[SubscriptionPlan](cachex.HybridCacheConfig[SubscriptionPlan]{
  76. Namespace: cachex.Namespace(subscriptionPlanCacheNamespace),
  77. Redis: common.RDB,
  78. RedisEnabled: func() bool {
  79. return common.RedisEnabled && common.RDB != nil
  80. },
  81. RedisCodec: cachex.JSONCodec[SubscriptionPlan]{},
  82. Memory: func() *hot.HotCache[string, SubscriptionPlan] {
  83. return hot.NewHotCache[string, SubscriptionPlan](hot.LRU, subscriptionPlanCacheCapacity()).
  84. WithTTL(ttl).
  85. WithJanitor().
  86. Build()
  87. },
  88. })
  89. })
  90. return subscriptionPlanCache
  91. }
  92. func getSubscriptionPlanInfoCache() *cachex.HybridCache[SubscriptionPlanInfo] {
  93. subscriptionPlanInfoCacheOnce.Do(func() {
  94. ttl := subscriptionPlanInfoCacheTTL()
  95. subscriptionPlanInfoCache = cachex.NewHybridCache[SubscriptionPlanInfo](cachex.HybridCacheConfig[SubscriptionPlanInfo]{
  96. Namespace: cachex.Namespace(subscriptionPlanInfoCacheNamespace),
  97. Redis: common.RDB,
  98. RedisEnabled: func() bool {
  99. return common.RedisEnabled && common.RDB != nil
  100. },
  101. RedisCodec: cachex.JSONCodec[SubscriptionPlanInfo]{},
  102. Memory: func() *hot.HotCache[string, SubscriptionPlanInfo] {
  103. return hot.NewHotCache[string, SubscriptionPlanInfo](hot.LRU, subscriptionPlanInfoCacheCapacity()).
  104. WithTTL(ttl).
  105. WithJanitor().
  106. Build()
  107. },
  108. })
  109. })
  110. return subscriptionPlanInfoCache
  111. }
  112. func subscriptionPlanCacheKey(id int) string {
  113. if id <= 0 {
  114. return ""
  115. }
  116. return strconv.Itoa(id)
  117. }
  118. func InvalidateSubscriptionPlanCache(planId int) {
  119. if planId <= 0 {
  120. return
  121. }
  122. cache := getSubscriptionPlanCache()
  123. _, _ = cache.DeleteMany([]string{subscriptionPlanCacheKey(planId)})
  124. infoCache := getSubscriptionPlanInfoCache()
  125. _ = infoCache.Purge()
  126. }
  127. // Subscription plan
  128. type SubscriptionPlan struct {
  129. Id int `json:"id"`
  130. Title string `json:"title" gorm:"type:varchar(128);not null"`
  131. Subtitle string `json:"subtitle" gorm:"type:varchar(255);default:''"`
  132. // Display money amount (follow existing code style: float64 for money)
  133. PriceAmount float64 `json:"price_amount" gorm:"type:decimal(10,6);not null;default:0"`
  134. Currency string `json:"currency" gorm:"type:varchar(8);not null;default:'USD'"`
  135. DurationUnit string `json:"duration_unit" gorm:"type:varchar(16);not null;default:'month'"`
  136. DurationValue int `json:"duration_value" gorm:"type:int;not null;default:1"`
  137. CustomSeconds int64 `json:"custom_seconds" gorm:"type:bigint;not null;default:0"`
  138. Enabled bool `json:"enabled" gorm:"default:true"`
  139. SortOrder int `json:"sort_order" gorm:"type:int;default:0"`
  140. StripePriceId string `json:"stripe_price_id" gorm:"type:varchar(128);default:''"`
  141. CreemProductId string `json:"creem_product_id" gorm:"type:varchar(128);default:''"`
  142. // Max purchases per user (0 = unlimited)
  143. MaxPurchasePerUser int `json:"max_purchase_per_user" gorm:"type:int;default:0"`
  144. // Upgrade user group after purchase (empty = no change)
  145. UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"`
  146. // Total quota (amount in quota units, 0 = unlimited)
  147. TotalAmount int64 `json:"total_amount" gorm:"type:bigint;not null;default:0"`
  148. // Quota reset period for plan
  149. QuotaResetPeriod string `json:"quota_reset_period" gorm:"type:varchar(16);default:'never'"`
  150. QuotaResetCustomSeconds int64 `json:"quota_reset_custom_seconds" gorm:"type:bigint;default:0"`
  151. CreatedAt int64 `json:"created_at" gorm:"bigint"`
  152. UpdatedAt int64 `json:"updated_at" gorm:"bigint"`
  153. }
  154. func (p *SubscriptionPlan) BeforeCreate(tx *gorm.DB) error {
  155. now := common.GetTimestamp()
  156. p.CreatedAt = now
  157. p.UpdatedAt = now
  158. return nil
  159. }
  160. func (p *SubscriptionPlan) BeforeUpdate(tx *gorm.DB) error {
  161. p.UpdatedAt = common.GetTimestamp()
  162. return nil
  163. }
  164. // Subscription order (payment -> webhook -> create UserSubscription)
  165. type SubscriptionOrder struct {
  166. Id int `json:"id"`
  167. UserId int `json:"user_id" gorm:"index"`
  168. PlanId int `json:"plan_id" gorm:"index"`
  169. Money float64 `json:"money"`
  170. TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
  171. PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
  172. PaymentProvider string `json:"payment_provider" gorm:"type:varchar(50);default:''"`
  173. Status string `json:"status"`
  174. CreateTime int64 `json:"create_time"`
  175. CompleteTime int64 `json:"complete_time"`
  176. ProviderPayload string `json:"provider_payload" gorm:"type:text"`
  177. }
  178. func (o *SubscriptionOrder) Insert() error {
  179. if o.CreateTime == 0 {
  180. o.CreateTime = common.GetTimestamp()
  181. }
  182. return DB.Create(o).Error
  183. }
  184. func (o *SubscriptionOrder) Update() error {
  185. return DB.Save(o).Error
  186. }
  187. func GetSubscriptionOrderByTradeNo(tradeNo string) *SubscriptionOrder {
  188. if tradeNo == "" {
  189. return nil
  190. }
  191. var order SubscriptionOrder
  192. if err := DB.Where("trade_no = ?", tradeNo).First(&order).Error; err != nil {
  193. return nil
  194. }
  195. return &order
  196. }
  197. // User subscription instance
  198. type UserSubscription struct {
  199. Id int `json:"id"`
  200. UserId int `json:"user_id" gorm:"index;index:idx_user_sub_active,priority:1"`
  201. PlanId int `json:"plan_id" gorm:"index"`
  202. AmountTotal int64 `json:"amount_total" gorm:"type:bigint;not null;default:0"`
  203. AmountUsed int64 `json:"amount_used" gorm:"type:bigint;not null;default:0"`
  204. StartTime int64 `json:"start_time" gorm:"bigint"`
  205. EndTime int64 `json:"end_time" gorm:"bigint;index;index:idx_user_sub_active,priority:3"`
  206. Status string `json:"status" gorm:"type:varchar(32);index;index:idx_user_sub_active,priority:2"` // active/expired/cancelled
  207. Source string `json:"source" gorm:"type:varchar(32);default:'order'"` // order/admin
  208. LastResetTime int64 `json:"last_reset_time" gorm:"type:bigint;default:0"`
  209. NextResetTime int64 `json:"next_reset_time" gorm:"type:bigint;default:0;index"`
  210. UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"`
  211. PrevUserGroup string `json:"prev_user_group" gorm:"type:varchar(64);default:''"`
  212. CreatedAt int64 `json:"created_at" gorm:"bigint"`
  213. UpdatedAt int64 `json:"updated_at" gorm:"bigint"`
  214. }
  215. func (s *UserSubscription) BeforeCreate(tx *gorm.DB) error {
  216. now := common.GetTimestamp()
  217. s.CreatedAt = now
  218. s.UpdatedAt = now
  219. return nil
  220. }
  221. func (s *UserSubscription) BeforeUpdate(tx *gorm.DB) error {
  222. s.UpdatedAt = common.GetTimestamp()
  223. return nil
  224. }
  225. type SubscriptionSummary struct {
  226. Subscription *UserSubscription `json:"subscription"`
  227. }
  228. func calcPlanEndTime(start time.Time, plan *SubscriptionPlan) (int64, error) {
  229. if plan == nil {
  230. return 0, errors.New("plan is nil")
  231. }
  232. if plan.DurationValue <= 0 && plan.DurationUnit != SubscriptionDurationCustom {
  233. return 0, errors.New("duration_value must be > 0")
  234. }
  235. switch plan.DurationUnit {
  236. case SubscriptionDurationYear:
  237. return start.AddDate(plan.DurationValue, 0, 0).Unix(), nil
  238. case SubscriptionDurationMonth:
  239. return start.AddDate(0, plan.DurationValue, 0).Unix(), nil
  240. case SubscriptionDurationDay:
  241. return start.Add(time.Duration(plan.DurationValue) * 24 * time.Hour).Unix(), nil
  242. case SubscriptionDurationHour:
  243. return start.Add(time.Duration(plan.DurationValue) * time.Hour).Unix(), nil
  244. case SubscriptionDurationCustom:
  245. if plan.CustomSeconds <= 0 {
  246. return 0, errors.New("custom_seconds must be > 0")
  247. }
  248. return start.Add(time.Duration(plan.CustomSeconds) * time.Second).Unix(), nil
  249. default:
  250. return 0, fmt.Errorf("invalid duration_unit: %s", plan.DurationUnit)
  251. }
  252. }
  253. func NormalizeResetPeriod(period string) string {
  254. switch strings.TrimSpace(period) {
  255. case SubscriptionResetDaily, SubscriptionResetWeekly, SubscriptionResetMonthly, SubscriptionResetCustom:
  256. return strings.TrimSpace(period)
  257. default:
  258. return SubscriptionResetNever
  259. }
  260. }
  261. func calcNextResetTime(base time.Time, plan *SubscriptionPlan, endUnix int64) int64 {
  262. if plan == nil {
  263. return 0
  264. }
  265. period := NormalizeResetPeriod(plan.QuotaResetPeriod)
  266. if period == SubscriptionResetNever {
  267. return 0
  268. }
  269. var next time.Time
  270. switch period {
  271. case SubscriptionResetDaily:
  272. next = time.Date(base.Year(), base.Month(), base.Day(), 0, 0, 0, 0, base.Location()).
  273. AddDate(0, 0, 1)
  274. case SubscriptionResetWeekly:
  275. // Align to next Monday 00:00
  276. weekday := int(base.Weekday()) // Sunday=0
  277. // Convert to Monday=1..Sunday=7
  278. if weekday == 0 {
  279. weekday = 7
  280. }
  281. daysUntil := 8 - weekday
  282. next = time.Date(base.Year(), base.Month(), base.Day(), 0, 0, 0, 0, base.Location()).
  283. AddDate(0, 0, daysUntil)
  284. case SubscriptionResetMonthly:
  285. // Align to first day of next month 00:00
  286. next = time.Date(base.Year(), base.Month(), 1, 0, 0, 0, 0, base.Location()).
  287. AddDate(0, 1, 0)
  288. case SubscriptionResetCustom:
  289. if plan.QuotaResetCustomSeconds <= 0 {
  290. return 0
  291. }
  292. next = base.Add(time.Duration(plan.QuotaResetCustomSeconds) * time.Second)
  293. default:
  294. return 0
  295. }
  296. if endUnix > 0 && next.Unix() > endUnix {
  297. return 0
  298. }
  299. return next.Unix()
  300. }
  301. func GetSubscriptionPlanById(id int) (*SubscriptionPlan, error) {
  302. return getSubscriptionPlanByIdTx(nil, id)
  303. }
  304. func getSubscriptionPlanByIdTx(tx *gorm.DB, id int) (*SubscriptionPlan, error) {
  305. if id <= 0 {
  306. return nil, errors.New("invalid plan id")
  307. }
  308. key := subscriptionPlanCacheKey(id)
  309. if key != "" {
  310. if cached, found, err := getSubscriptionPlanCache().Get(key); err == nil && found {
  311. return &cached, nil
  312. }
  313. }
  314. var plan SubscriptionPlan
  315. query := DB
  316. if tx != nil {
  317. query = tx
  318. }
  319. if err := query.Where("id = ?", id).First(&plan).Error; err != nil {
  320. return nil, err
  321. }
  322. _ = getSubscriptionPlanCache().SetWithTTL(key, plan, subscriptionPlanCacheTTL())
  323. return &plan, nil
  324. }
  325. func CountUserSubscriptionsByPlan(userId int, planId int) (int64, error) {
  326. if userId <= 0 || planId <= 0 {
  327. return 0, errors.New("invalid userId or planId")
  328. }
  329. var count int64
  330. if err := DB.Model(&UserSubscription{}).
  331. Where("user_id = ? AND plan_id = ?", userId, planId).
  332. Count(&count).Error; err != nil {
  333. return 0, err
  334. }
  335. return count, nil
  336. }
  337. func getUserGroupByIdTx(tx *gorm.DB, userId int) (string, error) {
  338. if userId <= 0 {
  339. return "", errors.New("invalid userId")
  340. }
  341. if tx == nil {
  342. tx = DB
  343. }
  344. var group string
  345. if err := tx.Model(&User{}).Where("id = ?", userId).Select(commonGroupCol).Find(&group).Error; err != nil {
  346. return "", err
  347. }
  348. return group, nil
  349. }
  350. func downgradeUserGroupForSubscriptionTx(tx *gorm.DB, sub *UserSubscription, now int64) (string, error) {
  351. if tx == nil || sub == nil {
  352. return "", errors.New("invalid downgrade args")
  353. }
  354. upgradeGroup := strings.TrimSpace(sub.UpgradeGroup)
  355. if upgradeGroup == "" {
  356. return "", nil
  357. }
  358. currentGroup, err := getUserGroupByIdTx(tx, sub.UserId)
  359. if err != nil {
  360. return "", err
  361. }
  362. if currentGroup != upgradeGroup {
  363. return "", nil
  364. }
  365. var activeSub UserSubscription
  366. activeQuery := tx.Where("user_id = ? AND status = ? AND end_time > ? AND id <> ? AND upgrade_group <> ''",
  367. sub.UserId, "active", now, sub.Id).
  368. Order("end_time desc, id desc").
  369. Limit(1).
  370. Find(&activeSub)
  371. if activeQuery.Error == nil && activeQuery.RowsAffected > 0 {
  372. return "", nil
  373. }
  374. prevGroup := strings.TrimSpace(sub.PrevUserGroup)
  375. if prevGroup == "" || prevGroup == currentGroup {
  376. return "", nil
  377. }
  378. if err := tx.Model(&User{}).Where("id = ?", sub.UserId).
  379. Update("group", prevGroup).Error; err != nil {
  380. return "", err
  381. }
  382. return prevGroup, nil
  383. }
  384. func CreateUserSubscriptionFromPlanTx(tx *gorm.DB, userId int, plan *SubscriptionPlan, source string) (*UserSubscription, error) {
  385. if tx == nil {
  386. return nil, errors.New("tx is nil")
  387. }
  388. if plan == nil || plan.Id == 0 {
  389. return nil, errors.New("invalid plan")
  390. }
  391. if userId <= 0 {
  392. return nil, errors.New("invalid user id")
  393. }
  394. if plan.MaxPurchasePerUser > 0 {
  395. var count int64
  396. if err := tx.Model(&UserSubscription{}).
  397. Where("user_id = ? AND plan_id = ?", userId, plan.Id).
  398. Count(&count).Error; err != nil {
  399. return nil, err
  400. }
  401. if count >= int64(plan.MaxPurchasePerUser) {
  402. return nil, errors.New("已达到该套餐购买上限")
  403. }
  404. }
  405. nowUnix := GetDBTimestamp()
  406. now := time.Unix(nowUnix, 0)
  407. endUnix, err := calcPlanEndTime(now, plan)
  408. if err != nil {
  409. return nil, err
  410. }
  411. resetBase := now
  412. nextReset := calcNextResetTime(resetBase, plan, endUnix)
  413. lastReset := int64(0)
  414. if nextReset > 0 {
  415. lastReset = now.Unix()
  416. }
  417. upgradeGroup := strings.TrimSpace(plan.UpgradeGroup)
  418. prevGroup := ""
  419. if upgradeGroup != "" {
  420. currentGroup, err := getUserGroupByIdTx(tx, userId)
  421. if err != nil {
  422. return nil, err
  423. }
  424. if currentGroup != upgradeGroup {
  425. prevGroup = currentGroup
  426. if err := tx.Model(&User{}).Where("id = ?", userId).
  427. Update("group", upgradeGroup).Error; err != nil {
  428. return nil, err
  429. }
  430. }
  431. }
  432. sub := &UserSubscription{
  433. UserId: userId,
  434. PlanId: plan.Id,
  435. AmountTotal: plan.TotalAmount,
  436. AmountUsed: 0,
  437. StartTime: now.Unix(),
  438. EndTime: endUnix,
  439. Status: "active",
  440. Source: source,
  441. LastResetTime: lastReset,
  442. NextResetTime: nextReset,
  443. UpgradeGroup: upgradeGroup,
  444. PrevUserGroup: prevGroup,
  445. CreatedAt: common.GetTimestamp(),
  446. UpdatedAt: common.GetTimestamp(),
  447. }
  448. if err := tx.Create(sub).Error; err != nil {
  449. return nil, err
  450. }
  451. return sub, nil
  452. }
  453. // Complete a subscription order (idempotent). Creates a UserSubscription snapshot from the plan.
  454. // expectedPaymentProvider guards against cross-gateway callback attacks (empty skips the check).
  455. // actualPaymentMethod updates the order's PaymentMethod to reflect the real payment type used (empty skips update).
  456. func CompleteSubscriptionOrder(tradeNo string, providerPayload string, expectedPaymentProvider string, actualPaymentMethod string) error {
  457. if tradeNo == "" {
  458. return errors.New("tradeNo is empty")
  459. }
  460. refCol := "`trade_no`"
  461. if common.UsingPostgreSQL {
  462. refCol = `"trade_no"`
  463. }
  464. var logUserId int
  465. var logPlanTitle string
  466. var logMoney float64
  467. var logPaymentMethod string
  468. var upgradeGroup string
  469. err := DB.Transaction(func(tx *gorm.DB) error {
  470. var order SubscriptionOrder
  471. if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(&order).Error; err != nil {
  472. return ErrSubscriptionOrderNotFound
  473. }
  474. if expectedPaymentProvider != "" && order.PaymentProvider != expectedPaymentProvider {
  475. return ErrPaymentMethodMismatch
  476. }
  477. if order.Status == common.TopUpStatusSuccess {
  478. return nil
  479. }
  480. if order.Status != common.TopUpStatusPending {
  481. return ErrSubscriptionOrderStatusInvalid
  482. }
  483. plan, err := GetSubscriptionPlanById(order.PlanId)
  484. if err != nil {
  485. return err
  486. }
  487. if !plan.Enabled {
  488. // still allow completion for already purchased orders
  489. }
  490. upgradeGroup = strings.TrimSpace(plan.UpgradeGroup)
  491. _, err = CreateUserSubscriptionFromPlanTx(tx, order.UserId, plan, "order")
  492. if err != nil {
  493. return err
  494. }
  495. if err := upsertSubscriptionTopUpTx(tx, &order); err != nil {
  496. return err
  497. }
  498. order.Status = common.TopUpStatusSuccess
  499. order.CompleteTime = common.GetTimestamp()
  500. if providerPayload != "" {
  501. order.ProviderPayload = providerPayload
  502. }
  503. if actualPaymentMethod != "" && order.PaymentMethod != actualPaymentMethod {
  504. order.PaymentMethod = actualPaymentMethod
  505. }
  506. if err := tx.Save(&order).Error; err != nil {
  507. return err
  508. }
  509. logUserId = order.UserId
  510. logPlanTitle = plan.Title
  511. logMoney = order.Money
  512. logPaymentMethod = order.PaymentMethod
  513. return nil
  514. })
  515. if err != nil {
  516. return err
  517. }
  518. if upgradeGroup != "" && logUserId > 0 {
  519. _ = UpdateUserGroupCache(logUserId, upgradeGroup)
  520. }
  521. if logUserId > 0 {
  522. msg := fmt.Sprintf("订阅购买成功,套餐: %s,支付金额: %.2f,支付方式: %s", logPlanTitle, logMoney, logPaymentMethod)
  523. RecordLog(logUserId, LogTypeTopup, msg)
  524. }
  525. return nil
  526. }
  527. func upsertSubscriptionTopUpTx(tx *gorm.DB, order *SubscriptionOrder) error {
  528. if tx == nil || order == nil {
  529. return errors.New("invalid subscription order")
  530. }
  531. now := common.GetTimestamp()
  532. var topup TopUp
  533. if err := tx.Where("trade_no = ?", order.TradeNo).First(&topup).Error; err != nil {
  534. if errors.Is(err, gorm.ErrRecordNotFound) {
  535. topup = TopUp{
  536. UserId: order.UserId,
  537. Amount: 0,
  538. Money: order.Money,
  539. TradeNo: order.TradeNo,
  540. PaymentMethod: order.PaymentMethod,
  541. CreateTime: order.CreateTime,
  542. CompleteTime: now,
  543. Status: common.TopUpStatusSuccess,
  544. }
  545. return tx.Create(&topup).Error
  546. }
  547. return err
  548. }
  549. topup.Money = order.Money
  550. if topup.PaymentMethod == "" {
  551. topup.PaymentMethod = order.PaymentMethod
  552. } else if topup.PaymentMethod != order.PaymentMethod {
  553. return ErrPaymentMethodMismatch
  554. }
  555. if topup.CreateTime == 0 {
  556. topup.CreateTime = order.CreateTime
  557. }
  558. topup.CompleteTime = now
  559. topup.Status = common.TopUpStatusSuccess
  560. return tx.Save(&topup).Error
  561. }
  562. func ExpireSubscriptionOrder(tradeNo string, expectedPaymentProvider string) error {
  563. if tradeNo == "" {
  564. return errors.New("tradeNo is empty")
  565. }
  566. refCol := "`trade_no`"
  567. if common.UsingPostgreSQL {
  568. refCol = `"trade_no"`
  569. }
  570. return DB.Transaction(func(tx *gorm.DB) error {
  571. var order SubscriptionOrder
  572. if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(&order).Error; err != nil {
  573. return ErrSubscriptionOrderNotFound
  574. }
  575. if expectedPaymentProvider != "" && order.PaymentProvider != expectedPaymentProvider {
  576. return ErrPaymentMethodMismatch
  577. }
  578. if order.Status != common.TopUpStatusPending {
  579. return nil
  580. }
  581. order.Status = common.TopUpStatusExpired
  582. order.CompleteTime = common.GetTimestamp()
  583. return tx.Save(&order).Error
  584. })
  585. }
  586. // Admin bind (no payment). Creates a UserSubscription from a plan.
  587. func AdminBindSubscription(userId int, planId int, sourceNote string) (string, error) {
  588. if userId <= 0 || planId <= 0 {
  589. return "", errors.New("invalid userId or planId")
  590. }
  591. plan, err := GetSubscriptionPlanById(planId)
  592. if err != nil {
  593. return "", err
  594. }
  595. err = DB.Transaction(func(tx *gorm.DB) error {
  596. _, err := CreateUserSubscriptionFromPlanTx(tx, userId, plan, "admin")
  597. return err
  598. })
  599. if err != nil {
  600. return "", err
  601. }
  602. if strings.TrimSpace(plan.UpgradeGroup) != "" {
  603. _ = UpdateUserGroupCache(userId, plan.UpgradeGroup)
  604. return fmt.Sprintf("用户分组将升级到 %s", plan.UpgradeGroup), nil
  605. }
  606. return "", nil
  607. }
  608. // GetAllActiveUserSubscriptions returns all active subscriptions for a user.
  609. func GetAllActiveUserSubscriptions(userId int) ([]SubscriptionSummary, error) {
  610. if userId <= 0 {
  611. return nil, errors.New("invalid userId")
  612. }
  613. now := common.GetTimestamp()
  614. var subs []UserSubscription
  615. err := DB.Where("user_id = ? AND status = ? AND end_time > ?", userId, "active", now).
  616. Order("end_time desc, id desc").
  617. Find(&subs).Error
  618. if err != nil {
  619. return nil, err
  620. }
  621. return buildSubscriptionSummaries(subs), nil
  622. }
  623. // HasActiveUserSubscription returns whether the user has any active subscription.
  624. // This is a lightweight existence check to avoid heavy pre-consume transactions.
  625. func HasActiveUserSubscription(userId int) (bool, error) {
  626. if userId <= 0 {
  627. return false, errors.New("invalid userId")
  628. }
  629. now := common.GetTimestamp()
  630. var count int64
  631. if err := DB.Model(&UserSubscription{}).
  632. Where("user_id = ? AND status = ? AND end_time > ?", userId, "active", now).
  633. Count(&count).Error; err != nil {
  634. return false, err
  635. }
  636. return count > 0, nil
  637. }
  638. // GetAllUserSubscriptions returns all subscriptions (active and expired) for a user.
  639. func GetAllUserSubscriptions(userId int) ([]SubscriptionSummary, error) {
  640. if userId <= 0 {
  641. return nil, errors.New("invalid userId")
  642. }
  643. var subs []UserSubscription
  644. err := DB.Where("user_id = ?", userId).
  645. Order("end_time desc, id desc").
  646. Find(&subs).Error
  647. if err != nil {
  648. return nil, err
  649. }
  650. return buildSubscriptionSummaries(subs), nil
  651. }
  652. func buildSubscriptionSummaries(subs []UserSubscription) []SubscriptionSummary {
  653. if len(subs) == 0 {
  654. return []SubscriptionSummary{}
  655. }
  656. result := make([]SubscriptionSummary, 0, len(subs))
  657. for _, sub := range subs {
  658. subCopy := sub
  659. result = append(result, SubscriptionSummary{
  660. Subscription: &subCopy,
  661. })
  662. }
  663. return result
  664. }
  665. // AdminInvalidateUserSubscription marks a user subscription as cancelled and ends it immediately.
  666. func AdminInvalidateUserSubscription(userSubscriptionId int) (string, error) {
  667. if userSubscriptionId <= 0 {
  668. return "", errors.New("invalid userSubscriptionId")
  669. }
  670. now := common.GetTimestamp()
  671. cacheGroup := ""
  672. downgradeGroup := ""
  673. var userId int
  674. err := DB.Transaction(func(tx *gorm.DB) error {
  675. var sub UserSubscription
  676. if err := tx.Set("gorm:query_option", "FOR UPDATE").
  677. Where("id = ?", userSubscriptionId).First(&sub).Error; err != nil {
  678. return err
  679. }
  680. userId = sub.UserId
  681. if err := tx.Model(&sub).Updates(map[string]interface{}{
  682. "status": "cancelled",
  683. "end_time": now,
  684. "updated_at": now,
  685. }).Error; err != nil {
  686. return err
  687. }
  688. target, err := downgradeUserGroupForSubscriptionTx(tx, &sub, now)
  689. if err != nil {
  690. return err
  691. }
  692. if target != "" {
  693. cacheGroup = target
  694. downgradeGroup = target
  695. }
  696. return nil
  697. })
  698. if err != nil {
  699. return "", err
  700. }
  701. if cacheGroup != "" && userId > 0 {
  702. _ = UpdateUserGroupCache(userId, cacheGroup)
  703. }
  704. if downgradeGroup != "" {
  705. return fmt.Sprintf("用户分组将回退到 %s", downgradeGroup), nil
  706. }
  707. return "", nil
  708. }
  709. // AdminDeleteUserSubscription hard-deletes a user subscription.
  710. func AdminDeleteUserSubscription(userSubscriptionId int) (string, error) {
  711. if userSubscriptionId <= 0 {
  712. return "", errors.New("invalid userSubscriptionId")
  713. }
  714. now := common.GetTimestamp()
  715. cacheGroup := ""
  716. downgradeGroup := ""
  717. var userId int
  718. err := DB.Transaction(func(tx *gorm.DB) error {
  719. var sub UserSubscription
  720. if err := tx.Set("gorm:query_option", "FOR UPDATE").
  721. Where("id = ?", userSubscriptionId).First(&sub).Error; err != nil {
  722. return err
  723. }
  724. userId = sub.UserId
  725. target, err := downgradeUserGroupForSubscriptionTx(tx, &sub, now)
  726. if err != nil {
  727. return err
  728. }
  729. if target != "" {
  730. cacheGroup = target
  731. downgradeGroup = target
  732. }
  733. if err := tx.Where("id = ?", userSubscriptionId).Delete(&UserSubscription{}).Error; err != nil {
  734. return err
  735. }
  736. return nil
  737. })
  738. if err != nil {
  739. return "", err
  740. }
  741. if cacheGroup != "" && userId > 0 {
  742. _ = UpdateUserGroupCache(userId, cacheGroup)
  743. }
  744. if downgradeGroup != "" {
  745. return fmt.Sprintf("用户分组将回退到 %s", downgradeGroup), nil
  746. }
  747. return "", nil
  748. }
  749. type SubscriptionPreConsumeResult struct {
  750. UserSubscriptionId int
  751. PreConsumed int64
  752. AmountTotal int64
  753. AmountUsedBefore int64
  754. AmountUsedAfter int64
  755. }
  756. // ExpireDueSubscriptions marks expired subscriptions and handles group downgrade.
  757. func ExpireDueSubscriptions(limit int) (int, error) {
  758. if limit <= 0 {
  759. limit = 200
  760. }
  761. now := GetDBTimestamp()
  762. var subs []UserSubscription
  763. if err := DB.Where("status = ? AND end_time > 0 AND end_time <= ?", "active", now).
  764. Order("end_time asc, id asc").
  765. Limit(limit).
  766. Find(&subs).Error; err != nil {
  767. return 0, err
  768. }
  769. if len(subs) == 0 {
  770. return 0, nil
  771. }
  772. expiredCount := 0
  773. userIds := make(map[int]struct{}, len(subs))
  774. for _, sub := range subs {
  775. if sub.UserId > 0 {
  776. userIds[sub.UserId] = struct{}{}
  777. }
  778. }
  779. for userId := range userIds {
  780. cacheGroup := ""
  781. err := DB.Transaction(func(tx *gorm.DB) error {
  782. res := tx.Model(&UserSubscription{}).
  783. Where("user_id = ? AND status = ? AND end_time > 0 AND end_time <= ?", userId, "active", now).
  784. Updates(map[string]interface{}{
  785. "status": "expired",
  786. "updated_at": common.GetTimestamp(),
  787. })
  788. if res.Error != nil {
  789. return res.Error
  790. }
  791. expiredCount += int(res.RowsAffected)
  792. // If there's an active upgraded subscription, keep current group.
  793. var activeSub UserSubscription
  794. activeQuery := tx.Where("user_id = ? AND status = ? AND end_time > ? AND upgrade_group <> ''",
  795. userId, "active", now).
  796. Order("end_time desc, id desc").
  797. Limit(1).
  798. Find(&activeSub)
  799. if activeQuery.Error == nil && activeQuery.RowsAffected > 0 {
  800. return nil
  801. }
  802. // No active upgraded subscription, downgrade to previous group if needed.
  803. var lastExpired UserSubscription
  804. expiredQuery := tx.Where("user_id = ? AND status = ? AND upgrade_group <> ''",
  805. userId, "expired").
  806. Order("end_time desc, id desc").
  807. Limit(1).
  808. Find(&lastExpired)
  809. if expiredQuery.Error != nil || expiredQuery.RowsAffected == 0 {
  810. return nil
  811. }
  812. upgradeGroup := strings.TrimSpace(lastExpired.UpgradeGroup)
  813. prevGroup := strings.TrimSpace(lastExpired.PrevUserGroup)
  814. if upgradeGroup == "" || prevGroup == "" {
  815. return nil
  816. }
  817. currentGroup, err := getUserGroupByIdTx(tx, userId)
  818. if err != nil {
  819. return err
  820. }
  821. if currentGroup != upgradeGroup || currentGroup == prevGroup {
  822. return nil
  823. }
  824. if err := tx.Model(&User{}).Where("id = ?", userId).
  825. Update("group", prevGroup).Error; err != nil {
  826. return err
  827. }
  828. cacheGroup = prevGroup
  829. return nil
  830. })
  831. if err != nil {
  832. return expiredCount, err
  833. }
  834. if cacheGroup != "" {
  835. _ = UpdateUserGroupCache(userId, cacheGroup)
  836. }
  837. }
  838. return expiredCount, nil
  839. }
  840. // SubscriptionPreConsumeRecord stores idempotent pre-consume operations per request.
  841. type SubscriptionPreConsumeRecord struct {
  842. Id int `json:"id"`
  843. RequestId string `json:"request_id" gorm:"type:varchar(64);uniqueIndex"`
  844. UserId int `json:"user_id" gorm:"index"`
  845. UserSubscriptionId int `json:"user_subscription_id" gorm:"index"`
  846. PreConsumed int64 `json:"pre_consumed" gorm:"type:bigint;not null;default:0"`
  847. Status string `json:"status" gorm:"type:varchar(32);index"` // consumed/refunded
  848. CreatedAt int64 `json:"created_at" gorm:"bigint"`
  849. UpdatedAt int64 `json:"updated_at" gorm:"bigint;index"`
  850. }
  851. func (r *SubscriptionPreConsumeRecord) BeforeCreate(tx *gorm.DB) error {
  852. now := common.GetTimestamp()
  853. r.CreatedAt = now
  854. r.UpdatedAt = now
  855. return nil
  856. }
  857. func (r *SubscriptionPreConsumeRecord) BeforeUpdate(tx *gorm.DB) error {
  858. r.UpdatedAt = common.GetTimestamp()
  859. return nil
  860. }
  861. func maybeResetUserSubscriptionWithPlanTx(tx *gorm.DB, sub *UserSubscription, plan *SubscriptionPlan, now int64) error {
  862. if tx == nil || sub == nil || plan == nil {
  863. return errors.New("invalid reset args")
  864. }
  865. if sub.NextResetTime > 0 && sub.NextResetTime > now {
  866. return nil
  867. }
  868. if NormalizeResetPeriod(plan.QuotaResetPeriod) == SubscriptionResetNever {
  869. return nil
  870. }
  871. baseUnix := sub.LastResetTime
  872. if baseUnix <= 0 {
  873. baseUnix = sub.StartTime
  874. }
  875. base := time.Unix(baseUnix, 0)
  876. next := calcNextResetTime(base, plan, sub.EndTime)
  877. advanced := false
  878. for next > 0 && next <= now {
  879. advanced = true
  880. base = time.Unix(next, 0)
  881. next = calcNextResetTime(base, plan, sub.EndTime)
  882. }
  883. if !advanced {
  884. if sub.NextResetTime == 0 && next > 0 {
  885. sub.NextResetTime = next
  886. sub.LastResetTime = base.Unix()
  887. return tx.Save(sub).Error
  888. }
  889. return nil
  890. }
  891. sub.AmountUsed = 0
  892. sub.LastResetTime = base.Unix()
  893. sub.NextResetTime = next
  894. return tx.Save(sub).Error
  895. }
  896. // PreConsumeUserSubscription pre-consumes from any active subscription total quota.
  897. func PreConsumeUserSubscription(requestId string, userId int, modelName string, quotaType int, amount int64) (*SubscriptionPreConsumeResult, error) {
  898. if userId <= 0 {
  899. return nil, errors.New("invalid userId")
  900. }
  901. if strings.TrimSpace(requestId) == "" {
  902. return nil, errors.New("requestId is empty")
  903. }
  904. if amount <= 0 {
  905. return nil, errors.New("amount must be > 0")
  906. }
  907. now := GetDBTimestamp()
  908. returnValue := &SubscriptionPreConsumeResult{}
  909. err := DB.Transaction(func(tx *gorm.DB) error {
  910. var existing SubscriptionPreConsumeRecord
  911. query := tx.Where("request_id = ?", requestId).Limit(1).Find(&existing)
  912. if query.Error != nil {
  913. return query.Error
  914. }
  915. if query.RowsAffected > 0 {
  916. if existing.Status == "refunded" {
  917. return errors.New("subscription pre-consume already refunded")
  918. }
  919. var sub UserSubscription
  920. if err := tx.Where("id = ?", existing.UserSubscriptionId).First(&sub).Error; err != nil {
  921. return err
  922. }
  923. returnValue.UserSubscriptionId = sub.Id
  924. returnValue.PreConsumed = existing.PreConsumed
  925. returnValue.AmountTotal = sub.AmountTotal
  926. returnValue.AmountUsedBefore = sub.AmountUsed
  927. returnValue.AmountUsedAfter = sub.AmountUsed
  928. return nil
  929. }
  930. var subs []UserSubscription
  931. if err := tx.Set("gorm:query_option", "FOR UPDATE").
  932. Where("user_id = ? AND status = ? AND end_time > ?", userId, "active", now).
  933. Order("end_time asc, id asc").
  934. Find(&subs).Error; err != nil {
  935. return errors.New("no active subscription")
  936. }
  937. if len(subs) == 0 {
  938. return errors.New("no active subscription")
  939. }
  940. for _, candidate := range subs {
  941. sub := candidate
  942. plan, err := getSubscriptionPlanByIdTx(tx, sub.PlanId)
  943. if err != nil {
  944. return err
  945. }
  946. if err := maybeResetUserSubscriptionWithPlanTx(tx, &sub, plan, now); err != nil {
  947. return err
  948. }
  949. usedBefore := sub.AmountUsed
  950. if sub.AmountTotal > 0 {
  951. remain := sub.AmountTotal - usedBefore
  952. if remain < amount {
  953. continue
  954. }
  955. }
  956. record := &SubscriptionPreConsumeRecord{
  957. RequestId: requestId,
  958. UserId: userId,
  959. UserSubscriptionId: sub.Id,
  960. PreConsumed: amount,
  961. Status: "consumed",
  962. }
  963. if err := tx.Create(record).Error; err != nil {
  964. var dup SubscriptionPreConsumeRecord
  965. if err2 := tx.Where("request_id = ?", requestId).First(&dup).Error; err2 == nil {
  966. if dup.Status == "refunded" {
  967. return errors.New("subscription pre-consume already refunded")
  968. }
  969. returnValue.UserSubscriptionId = sub.Id
  970. returnValue.PreConsumed = dup.PreConsumed
  971. returnValue.AmountTotal = sub.AmountTotal
  972. returnValue.AmountUsedBefore = sub.AmountUsed
  973. returnValue.AmountUsedAfter = sub.AmountUsed
  974. return nil
  975. }
  976. return err
  977. }
  978. sub.AmountUsed += amount
  979. if err := tx.Save(&sub).Error; err != nil {
  980. return err
  981. }
  982. returnValue.UserSubscriptionId = sub.Id
  983. returnValue.PreConsumed = amount
  984. returnValue.AmountTotal = sub.AmountTotal
  985. returnValue.AmountUsedBefore = usedBefore
  986. returnValue.AmountUsedAfter = sub.AmountUsed
  987. return nil
  988. }
  989. return fmt.Errorf("subscription quota insufficient, need=%d", amount)
  990. })
  991. if err != nil {
  992. return nil, err
  993. }
  994. return returnValue, nil
  995. }
  996. // RefundSubscriptionPreConsume is idempotent and refunds pre-consumed subscription quota by requestId.
  997. func RefundSubscriptionPreConsume(requestId string) error {
  998. if strings.TrimSpace(requestId) == "" {
  999. return errors.New("requestId is empty")
  1000. }
  1001. return DB.Transaction(func(tx *gorm.DB) error {
  1002. var record SubscriptionPreConsumeRecord
  1003. if err := tx.Set("gorm:query_option", "FOR UPDATE").
  1004. Where("request_id = ?", requestId).First(&record).Error; err != nil {
  1005. return err
  1006. }
  1007. if record.Status == "refunded" {
  1008. return nil
  1009. }
  1010. if record.PreConsumed <= 0 {
  1011. record.Status = "refunded"
  1012. return tx.Save(&record).Error
  1013. }
  1014. if err := PostConsumeUserSubscriptionDelta(record.UserSubscriptionId, -record.PreConsumed); err != nil {
  1015. return err
  1016. }
  1017. record.Status = "refunded"
  1018. return tx.Save(&record).Error
  1019. })
  1020. }
  1021. // ResetDueSubscriptions resets subscriptions whose next_reset_time has passed.
  1022. func ResetDueSubscriptions(limit int) (int, error) {
  1023. if limit <= 0 {
  1024. limit = 200
  1025. }
  1026. now := GetDBTimestamp()
  1027. var subs []UserSubscription
  1028. if err := DB.Where("next_reset_time > 0 AND next_reset_time <= ? AND status = ?", now, "active").
  1029. Order("next_reset_time asc").
  1030. Limit(limit).
  1031. Find(&subs).Error; err != nil {
  1032. return 0, err
  1033. }
  1034. if len(subs) == 0 {
  1035. return 0, nil
  1036. }
  1037. resetCount := 0
  1038. for _, sub := range subs {
  1039. subCopy := sub
  1040. plan, err := getSubscriptionPlanByIdTx(nil, sub.PlanId)
  1041. if err != nil || plan == nil {
  1042. continue
  1043. }
  1044. err = DB.Transaction(func(tx *gorm.DB) error {
  1045. var locked UserSubscription
  1046. if err := tx.Set("gorm:query_option", "FOR UPDATE").
  1047. Where("id = ? AND next_reset_time > 0 AND next_reset_time <= ?", subCopy.Id, now).
  1048. First(&locked).Error; err != nil {
  1049. return nil
  1050. }
  1051. if err := maybeResetUserSubscriptionWithPlanTx(tx, &locked, plan, now); err != nil {
  1052. return err
  1053. }
  1054. resetCount++
  1055. return nil
  1056. })
  1057. if err != nil {
  1058. return resetCount, err
  1059. }
  1060. }
  1061. return resetCount, nil
  1062. }
  1063. // CleanupSubscriptionPreConsumeRecords removes old idempotency records to keep table small.
  1064. func CleanupSubscriptionPreConsumeRecords(olderThanSeconds int64) (int64, error) {
  1065. if olderThanSeconds <= 0 {
  1066. olderThanSeconds = 7 * 24 * 3600
  1067. }
  1068. cutoff := GetDBTimestamp() - olderThanSeconds
  1069. res := DB.Where("updated_at < ?", cutoff).Delete(&SubscriptionPreConsumeRecord{})
  1070. return res.RowsAffected, res.Error
  1071. }
  1072. type SubscriptionPlanInfo struct {
  1073. PlanId int
  1074. PlanTitle string
  1075. }
  1076. func GetSubscriptionPlanInfoByUserSubscriptionId(userSubscriptionId int) (*SubscriptionPlanInfo, error) {
  1077. if userSubscriptionId <= 0 {
  1078. return nil, errors.New("invalid userSubscriptionId")
  1079. }
  1080. cacheKey := fmt.Sprintf("sub:%d", userSubscriptionId)
  1081. if cached, found, err := getSubscriptionPlanInfoCache().Get(cacheKey); err == nil && found {
  1082. return &cached, nil
  1083. }
  1084. var sub UserSubscription
  1085. if err := DB.Where("id = ?", userSubscriptionId).First(&sub).Error; err != nil {
  1086. return nil, err
  1087. }
  1088. plan, err := getSubscriptionPlanByIdTx(nil, sub.PlanId)
  1089. if err != nil {
  1090. return nil, err
  1091. }
  1092. info := &SubscriptionPlanInfo{
  1093. PlanId: sub.PlanId,
  1094. PlanTitle: plan.Title,
  1095. }
  1096. _ = getSubscriptionPlanInfoCache().SetWithTTL(cacheKey, *info, subscriptionPlanInfoCacheTTL())
  1097. return info, nil
  1098. }
  1099. // Update subscription used amount by delta (positive consume more, negative refund).
  1100. func PostConsumeUserSubscriptionDelta(userSubscriptionId int, delta int64) error {
  1101. if userSubscriptionId <= 0 {
  1102. return errors.New("invalid userSubscriptionId")
  1103. }
  1104. if delta == 0 {
  1105. return nil
  1106. }
  1107. return DB.Transaction(func(tx *gorm.DB) error {
  1108. var sub UserSubscription
  1109. if err := tx.Set("gorm:query_option", "FOR UPDATE").
  1110. Where("id = ?", userSubscriptionId).
  1111. First(&sub).Error; err != nil {
  1112. return err
  1113. }
  1114. newUsed := sub.AmountUsed + delta
  1115. if newUsed < 0 {
  1116. newUsed = 0
  1117. }
  1118. if sub.AmountTotal > 0 && newUsed > sub.AmountTotal {
  1119. return fmt.Errorf("subscription used exceeds total, used=%d total=%d", newUsed, sub.AmountTotal)
  1120. }
  1121. sub.AmountUsed = newUsed
  1122. return tx.Save(&sub).Error
  1123. })
  1124. }