Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

230 строки
6.3 KiB

  1. package model
  2. import (
  3. "errors"
  4. "fmt"
  5. "strconv"
  6. "github.com/QuantumNous/new-api/common"
  7. "github.com/QuantumNous/new-api/logger"
  8. "gorm.io/gorm"
  9. )
  10. // ErrRedeemFailed is returned when redemption fails due to database error
  11. var ErrRedeemFailed = errors.New("redeem.failed")
  12. // ErrSyncedUserRedeemDenied is returned when a synced user tries to redeem a code.
  13. var ErrSyncedUserRedeemDenied = errors.New("redemption.synced_user_denied")
  14. type Redemption struct {
  15. Id int `json:"id"`
  16. UserId int `json:"user_id"`
  17. Key string `json:"key" gorm:"type:char(32);uniqueIndex"`
  18. Status int `json:"status" gorm:"default:1"`
  19. Name string `json:"name" gorm:"index"`
  20. Remark string `json:"remark" gorm:"index"`
  21. Quota int `json:"quota" gorm:"default:100"`
  22. CreatedTime int64 `json:"created_time" gorm:"bigint"`
  23. RedeemedTime int64 `json:"redeemed_time" gorm:"bigint"`
  24. Count int `json:"count" gorm:"-:all"` // only for api request
  25. UsedUserId int `json:"used_user_id"`
  26. DeletedAt gorm.DeletedAt `gorm:"index"`
  27. ExpiredTime int64 `json:"expired_time" gorm:"bigint"` // 过期时间,0 表示不过期
  28. }
  29. func GetAllRedemptions(startIdx int, num int) (redemptions []*Redemption, total int64, err error) {
  30. // 开始事务
  31. tx := DB.Begin()
  32. if tx.Error != nil {
  33. return nil, 0, tx.Error
  34. }
  35. defer func() {
  36. if r := recover(); r != nil {
  37. tx.Rollback()
  38. }
  39. }()
  40. // 获取总数
  41. err = tx.Model(&Redemption{}).Count(&total).Error
  42. if err != nil {
  43. tx.Rollback()
  44. return nil, 0, err
  45. }
  46. // 获取分页数据
  47. err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
  48. if err != nil {
  49. tx.Rollback()
  50. return nil, 0, err
  51. }
  52. // 提交事务
  53. if err = tx.Commit().Error; err != nil {
  54. return nil, 0, err
  55. }
  56. return redemptions, total, nil
  57. }
  58. func SearchRedemptions(keyword string, startIdx int, num int) (redemptions []*Redemption, total int64, err error) {
  59. tx := DB.Begin()
  60. if tx.Error != nil {
  61. return nil, 0, tx.Error
  62. }
  63. defer func() {
  64. if r := recover(); r != nil {
  65. tx.Rollback()
  66. }
  67. }()
  68. // Build query based on keyword type
  69. query := tx.Model(&Redemption{})
  70. // Only try to convert to ID if the string represents a valid integer
  71. if id, err := strconv.Atoi(keyword); err == nil {
  72. query = query.Where("id = ? OR name LIKE ? OR remark LIKE ?", id, keyword+"%", keyword+"%")
  73. } else {
  74. query = query.Where("name LIKE ? OR remark LIKE ?", keyword+"%", keyword+"%")
  75. }
  76. // Get total count
  77. err = query.Count(&total).Error
  78. if err != nil {
  79. tx.Rollback()
  80. return nil, 0, err
  81. }
  82. // Get paginated data
  83. err = query.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
  84. if err != nil {
  85. tx.Rollback()
  86. return nil, 0, err
  87. }
  88. if err = tx.Commit().Error; err != nil {
  89. return nil, 0, err
  90. }
  91. return redemptions, total, nil
  92. }
  93. func GetRedemptionById(id int) (*Redemption, error) {
  94. if id == 0 {
  95. return nil, errors.New("id 为空!")
  96. }
  97. redemption := Redemption{Id: id}
  98. var err error = nil
  99. err = DB.First(&redemption, "id = ?", id).Error
  100. return &redemption, err
  101. }
  102. func Redeem(key string, userId int) (quota int, err error) {
  103. if key == "" {
  104. return 0, errors.New("未提供兑换码")
  105. }
  106. if userId == 0 {
  107. return 0, errors.New("无效的 user id")
  108. }
  109. user := &User{}
  110. err = DB.Select("id", "source").First(user, "id = ?", userId).Error
  111. if err != nil {
  112. return 0, err
  113. }
  114. if user.IsSyncedUser() {
  115. return 0, ErrSyncedUserRedeemDenied
  116. }
  117. redemption := &Redemption{}
  118. keyCol := "`key`"
  119. if common.UsingPostgreSQL {
  120. keyCol = `"key"`
  121. }
  122. common.RandomSleep()
  123. err = DB.Transaction(func(tx *gorm.DB) error {
  124. err := tx.Set("gorm:query_option", "FOR UPDATE").Where(keyCol+" = ?", key).First(redemption).Error
  125. if err != nil {
  126. return errors.New("无效的兑换码")
  127. }
  128. if redemption.Status != common.RedemptionCodeStatusEnabled {
  129. return errors.New("该兑换码已被使用")
  130. }
  131. if redemption.ExpiredTime != 0 && redemption.ExpiredTime < common.GetTimestamp() {
  132. return errors.New("该兑换码已过期")
  133. }
  134. err = tx.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
  135. if err != nil {
  136. return err
  137. }
  138. redemption.RedeemedTime = common.GetTimestamp()
  139. redemption.Status = common.RedemptionCodeStatusUsed
  140. redemption.UsedUserId = userId
  141. err = tx.Save(redemption).Error
  142. return err
  143. })
  144. if err != nil {
  145. common.SysError("redemption failed: " + err.Error())
  146. return 0, ErrRedeemFailed
  147. }
  148. // 触发余额更新回调(在事务外)
  149. if quotaUpdateCallback != nil {
  150. newQuota, _ := GetUserQuota(userId, true)
  151. // 使用 defer/recover 保护回调
  152. func() {
  153. defer func() {
  154. if r := recover(); r != nil {
  155. common.SysError(fmt.Sprintf("quota update callback panic in Redeem: %v", r))
  156. }
  157. }()
  158. quotaUpdateCallback(userId, newQuota)
  159. }()
  160. }
  161. RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s,兑换码ID %d", logger.LogQuota(redemption.Quota), redemption.Id))
  162. return redemption.Quota, nil
  163. }
  164. func (redemption *Redemption) Insert() error {
  165. var err error
  166. err = DB.Create(redemption).Error
  167. return err
  168. }
  169. func (redemption *Redemption) SelectUpdate() error {
  170. // This can update zero values
  171. return DB.Model(redemption).Select("redeemed_time", "status").Updates(redemption).Error
  172. }
  173. // Update Make sure your token's fields is completed, because this will update non-zero values
  174. func (redemption *Redemption) Update() error {
  175. var err error
  176. err = DB.Model(redemption).Select("name", "remark", "status", "quota", "redeemed_time", "expired_time").Updates(redemption).Error
  177. return err
  178. }
  179. func (redemption *Redemption) Delete() error {
  180. var err error
  181. err = DB.Delete(redemption).Error
  182. return err
  183. }
  184. func DeleteRedemptionById(id int) (err error) {
  185. if id == 0 {
  186. return errors.New("id 为空!")
  187. }
  188. redemption := Redemption{Id: id}
  189. err = DB.Where(redemption).First(&redemption).Error
  190. if err != nil {
  191. return err
  192. }
  193. return redemption.Delete()
  194. }
  195. func DeleteInvalidRedemptions() (int64, error) {
  196. now := common.GetTimestamp()
  197. result := DB.Where("status IN ? OR (status = ? AND expired_time != 0 AND expired_time < ?)", []int{common.RedemptionCodeStatusUsed, common.RedemptionCodeStatusDisabled}, common.RedemptionCodeStatusEnabled, now).Delete(&Redemption{})
  198. return result.RowsAffected, result.Error
  199. }