Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 

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