25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 

410 satır
10 KiB

  1. package model
  2. import (
  3. "errors"
  4. "fmt"
  5. "github.com/QuantumNous/new-api/common"
  6. "github.com/QuantumNous/new-api/types"
  7. "github.com/QuantumNous/new-api/logger"
  8. "github.com/shopspring/decimal"
  9. "gorm.io/gorm"
  10. )
  11. type TopUp struct {
  12. Id int `json:"id"`
  13. UserId int `json:"user_id" gorm:"index"`
  14. Amount int64 `json:"amount"`
  15. Money float64 `json:"money"`
  16. TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
  17. PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
  18. CreateTime int64 `json:"create_time"`
  19. CompleteTime int64 `json:"complete_time"`
  20. Status string `json:"status"`
  21. UserEmail string `json:"user_email" gorm:"-"` // Join 查询时填充,非数据库字段
  22. }
  23. // fillTopUpEmails 批量填充 topup 记录的用户邮箱
  24. func fillTopUpEmails(topups []*TopUp) {
  25. if len(topups) == 0 {
  26. return
  27. }
  28. userIds := types.NewSet[int]()
  29. for _, t := range topups {
  30. userIds.Add(t.UserId)
  31. }
  32. var users []User
  33. if err := DB.Select("id, email").Where("id IN ?", userIds.Items()).Find(&users).Error; err != nil {
  34. common.SysError("fillTopUpEmails: " + err.Error())
  35. return
  36. }
  37. emailMap := make(map[int]string, len(users))
  38. for _, u := range users {
  39. emailMap[u.Id] = u.Email
  40. }
  41. for _, t := range topups {
  42. t.UserEmail = emailMap[t.UserId]
  43. }
  44. }
  45. func (topUp *TopUp) Insert() error {
  46. var err error
  47. err = DB.Create(topUp).Error
  48. return err
  49. }
  50. func (topUp *TopUp) Update() error {
  51. var err error
  52. err = DB.Save(topUp).Error
  53. return err
  54. }
  55. func GetTopUpById(id int) *TopUp {
  56. var topUp *TopUp
  57. var err error
  58. err = DB.Where("id = ?", id).First(&topUp).Error
  59. if err != nil {
  60. return nil
  61. }
  62. return topUp
  63. }
  64. func GetTopUpByTradeNo(tradeNo string) *TopUp {
  65. var topUp *TopUp
  66. var err error
  67. err = DB.Where("trade_no = ?", tradeNo).First(&topUp).Error
  68. if err != nil {
  69. return nil
  70. }
  71. return topUp
  72. }
  73. func Recharge(referenceId string, customerId string) (err error) {
  74. if referenceId == "" {
  75. return errors.New("未提供支付单号")
  76. }
  77. var quota float64
  78. topUp := &TopUp{}
  79. refCol := "`trade_no`"
  80. if common.UsingPostgreSQL {
  81. refCol = `"trade_no"`
  82. }
  83. err = DB.Transaction(func(tx *gorm.DB) error {
  84. err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", referenceId).First(topUp).Error
  85. if err != nil {
  86. return errors.New("充值订单不存在")
  87. }
  88. if topUp.Status != common.TopUpStatusPending {
  89. return errors.New("充值订单状态错误")
  90. }
  91. topUp.CompleteTime = common.GetTimestamp()
  92. topUp.Status = common.TopUpStatusSuccess
  93. err = tx.Save(topUp).Error
  94. if err != nil {
  95. return err
  96. }
  97. quota = topUp.Money * common.QuotaPerUnit
  98. err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(map[string]interface{}{"stripe_customer": customerId, "quota": gorm.Expr("quota + ?", quota)}).Error
  99. if err != nil {
  100. return err
  101. }
  102. return nil
  103. })
  104. if err != nil {
  105. common.SysError("topup failed: " + err.Error())
  106. return errors.New("充值失败,请稍后重试")
  107. }
  108. RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount))
  109. return nil
  110. }
  111. func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
  112. // Start transaction
  113. tx := DB.Begin()
  114. if tx.Error != nil {
  115. return nil, 0, tx.Error
  116. }
  117. defer func() {
  118. if r := recover(); r != nil {
  119. tx.Rollback()
  120. }
  121. }()
  122. // Get total count within transaction
  123. err = tx.Model(&TopUp{}).Where("user_id = ?", userId).Count(&total).Error
  124. if err != nil {
  125. tx.Rollback()
  126. return nil, 0, err
  127. }
  128. // Get paginated topups within same transaction
  129. err = tx.Where("user_id = ?", userId).Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error
  130. if err != nil {
  131. tx.Rollback()
  132. return nil, 0, err
  133. }
  134. // Commit transaction
  135. if err = tx.Commit().Error; err != nil {
  136. return nil, 0, err
  137. }
  138. fillTopUpEmails(topups)
  139. return topups, total, nil
  140. }
  141. // GetAllTopUps 获取全平台的充值记录(管理员使用)
  142. func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
  143. tx := DB.Begin()
  144. if tx.Error != nil {
  145. return nil, 0, tx.Error
  146. }
  147. defer func() {
  148. if r := recover(); r != nil {
  149. tx.Rollback()
  150. }
  151. }()
  152. if err = tx.Model(&TopUp{}).Count(&total).Error; err != nil {
  153. tx.Rollback()
  154. return nil, 0, err
  155. }
  156. if err = tx.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil {
  157. tx.Rollback()
  158. return nil, 0, err
  159. }
  160. if err = tx.Commit().Error; err != nil {
  161. return nil, 0, err
  162. }
  163. fillTopUpEmails(topups)
  164. return topups, total, nil
  165. }
  166. // SearchUserTopUps 按订单号搜索某用户的充值记录
  167. func SearchUserTopUps(userId int, keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
  168. tx := DB.Begin()
  169. if tx.Error != nil {
  170. return nil, 0, tx.Error
  171. }
  172. defer func() {
  173. if r := recover(); r != nil {
  174. tx.Rollback()
  175. }
  176. }()
  177. query := tx.Model(&TopUp{}).Where("user_id = ?", userId)
  178. if keyword != "" {
  179. like := "%%" + keyword + "%%"
  180. query = query.Where("trade_no LIKE ?", like)
  181. }
  182. if err = query.Count(&total).Error; err != nil {
  183. tx.Rollback()
  184. return nil, 0, err
  185. }
  186. if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil {
  187. tx.Rollback()
  188. return nil, 0, err
  189. }
  190. if err = tx.Commit().Error; err != nil {
  191. return nil, 0, err
  192. }
  193. fillTopUpEmails(topups)
  194. return topups, total, nil
  195. }
  196. // SearchAllTopUps 按订单号搜索全平台充值记录(管理员使用)
  197. func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
  198. tx := DB.Begin()
  199. if tx.Error != nil {
  200. return nil, 0, tx.Error
  201. }
  202. defer func() {
  203. if r := recover(); r != nil {
  204. tx.Rollback()
  205. }
  206. }()
  207. query := tx.Model(&TopUp{})
  208. if keyword != "" {
  209. like := "%%" + keyword + "%%"
  210. query = query.Where("trade_no LIKE ?", like)
  211. }
  212. if err = query.Count(&total).Error; err != nil {
  213. tx.Rollback()
  214. return nil, 0, err
  215. }
  216. if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil {
  217. tx.Rollback()
  218. return nil, 0, err
  219. }
  220. if err = tx.Commit().Error; err != nil {
  221. return nil, 0, err
  222. }
  223. fillTopUpEmails(topups)
  224. return topups, total, nil
  225. }
  226. // ManualCompleteTopUp 管理员手动完成订单并给用户充值
  227. func ManualCompleteTopUp(tradeNo string) error {
  228. if tradeNo == "" {
  229. return errors.New("未提供订单号")
  230. }
  231. refCol := "`trade_no`"
  232. if common.UsingPostgreSQL {
  233. refCol = `"trade_no"`
  234. }
  235. var userId int
  236. var quotaToAdd int
  237. var payMoney float64
  238. err := DB.Transaction(func(tx *gorm.DB) error {
  239. topUp := &TopUp{}
  240. // 行级锁,避免并发补单
  241. if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error; err != nil {
  242. return errors.New("充值订单不存在")
  243. }
  244. // 幂等处理:已成功直接返回
  245. if topUp.Status == common.TopUpStatusSuccess {
  246. return nil
  247. }
  248. if topUp.Status != common.TopUpStatusPending {
  249. return errors.New("订单状态不是待支付,无法补单")
  250. }
  251. // 计算应充值额度:
  252. // - Stripe/微信支付订单:Money 代表经分组倍率换算后的数量,直接 * QuotaPerUnit
  253. // - 其他订单(如易支付):Amount 为美元数量,* QuotaPerUnit
  254. if topUp.PaymentMethod == "stripe" || topUp.PaymentMethod == "wechat_pay" || topUp.PaymentMethod == "alipay" {
  255. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  256. quotaToAdd = int(decimal.NewFromFloat(topUp.Money).Mul(dQuotaPerUnit).IntPart())
  257. } else {
  258. dAmount := decimal.NewFromInt(topUp.Amount)
  259. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  260. quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart())
  261. }
  262. if quotaToAdd <= 0 {
  263. return errors.New("无效的充值额度")
  264. }
  265. // 标记完成
  266. topUp.CompleteTime = common.GetTimestamp()
  267. topUp.Status = common.TopUpStatusSuccess
  268. if err := tx.Save(topUp).Error; err != nil {
  269. return err
  270. }
  271. // 增加用户额度(立即写库,保持一致性)
  272. if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil {
  273. return err
  274. }
  275. userId = topUp.UserId
  276. payMoney = topUp.Money
  277. return nil
  278. })
  279. if err != nil {
  280. return err
  281. }
  282. // 事务外记录日志,避免阻塞
  283. RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney))
  284. return nil
  285. }
  286. func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) {
  287. if referenceId == "" {
  288. return errors.New("未提供支付单号")
  289. }
  290. var quota int64
  291. topUp := &TopUp{}
  292. refCol := "`trade_no`"
  293. if common.UsingPostgreSQL {
  294. refCol = `"trade_no"`
  295. }
  296. err = DB.Transaction(func(tx *gorm.DB) error {
  297. err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", referenceId).First(topUp).Error
  298. if err != nil {
  299. return errors.New("充值订单不存在")
  300. }
  301. if topUp.Status != common.TopUpStatusPending {
  302. return errors.New("充值订单状态错误")
  303. }
  304. topUp.CompleteTime = common.GetTimestamp()
  305. topUp.Status = common.TopUpStatusSuccess
  306. err = tx.Save(topUp).Error
  307. if err != nil {
  308. return err
  309. }
  310. // Creem 直接使用 Amount 作为充值额度(整数)
  311. quota = topUp.Amount
  312. // 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名
  313. updateFields := map[string]interface{}{
  314. "quota": gorm.Expr("quota + ?", quota),
  315. }
  316. // 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时)
  317. if customerEmail != "" {
  318. // 先检查用户当前邮箱是否为空
  319. var user User
  320. err = tx.Where("id = ?", topUp.UserId).First(&user).Error
  321. if err != nil {
  322. return err
  323. }
  324. // 如果用户邮箱为空,则更新为支付时使用的邮箱
  325. if user.Email == "" {
  326. updateFields["email"] = customerEmail
  327. }
  328. }
  329. err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(updateFields).Error
  330. if err != nil {
  331. return err
  332. }
  333. return nil
  334. })
  335. if err != nil {
  336. common.SysError("creem topup failed: " + err.Error())
  337. return errors.New("充值失败,请稍后重试")
  338. }
  339. RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money))
  340. return nil
  341. }