topup.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. package controller
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "strconv"
  7. "sync"
  8. "time"
  9. "github.com/QuantumNous/new-api/common"
  10. "github.com/QuantumNous/new-api/logger"
  11. "github.com/QuantumNous/new-api/model"
  12. "github.com/QuantumNous/new-api/service"
  13. "github.com/QuantumNous/new-api/setting"
  14. "github.com/QuantumNous/new-api/setting/operation_setting"
  15. "github.com/QuantumNous/new-api/setting/system_setting"
  16. "github.com/Calcium-Ion/go-epay/epay"
  17. "github.com/gin-gonic/gin"
  18. "github.com/samber/lo"
  19. "github.com/shopspring/decimal"
  20. )
  21. func GetTopUpInfo(c *gin.Context) {
  22. // 获取支付方式
  23. payMethods := operation_setting.PayMethods
  24. // 如果启用了 Stripe 支付,添加到支付方法列表
  25. if isStripeTopUpEnabled() {
  26. // 检查是否已经包含 Stripe
  27. hasStripe := false
  28. for _, method := range payMethods {
  29. if method["type"] == "stripe" {
  30. hasStripe = true
  31. break
  32. }
  33. }
  34. if !hasStripe {
  35. stripeMethod := map[string]string{
  36. "name": "Stripe",
  37. "type": "stripe",
  38. "color": "rgba(var(--semi-purple-5), 1)",
  39. "min_topup": strconv.Itoa(setting.StripeMinTopUp),
  40. }
  41. payMethods = append(payMethods, stripeMethod)
  42. }
  43. }
  44. // 如果启用了 Waffo 支付,添加到支付方法列表
  45. enableWaffo := isWaffoTopUpEnabled()
  46. if enableWaffo {
  47. hasWaffo := false
  48. for _, method := range payMethods {
  49. if method["type"] == model.PaymentMethodWaffo {
  50. hasWaffo = true
  51. break
  52. }
  53. }
  54. if !hasWaffo {
  55. waffoMethod := map[string]string{
  56. "name": "Waffo (Global Payment)",
  57. "type": model.PaymentMethodWaffo,
  58. "color": "rgba(var(--semi-blue-5), 1)",
  59. "min_topup": strconv.Itoa(setting.WaffoMinTopUp),
  60. }
  61. payMethods = append(payMethods, waffoMethod)
  62. }
  63. }
  64. enableWaffoPancake := isWaffoPancakeTopUpEnabled()
  65. if enableWaffoPancake {
  66. hasWaffoPancake := false
  67. for _, method := range payMethods {
  68. if method["type"] == model.PaymentMethodWaffoPancake {
  69. hasWaffoPancake = true
  70. break
  71. }
  72. }
  73. if !hasWaffoPancake {
  74. payMethods = append(payMethods, map[string]string{
  75. "name": "Waffo Pancake",
  76. "type": model.PaymentMethodWaffoPancake,
  77. "color": "rgba(var(--semi-orange-5), 1)",
  78. "min_topup": strconv.Itoa(setting.WaffoPancakeMinTopUp),
  79. })
  80. }
  81. }
  82. data := gin.H{
  83. "enable_online_topup": isEpayTopUpEnabled(),
  84. "enable_stripe_topup": isStripeTopUpEnabled(),
  85. "enable_creem_topup": isCreemTopUpEnabled(),
  86. "enable_waffo_topup": enableWaffo,
  87. "enable_waffo_pancake_topup": enableWaffoPancake,
  88. "waffo_pay_methods": func() interface{} {
  89. if enableWaffo {
  90. return setting.GetWaffoPayMethods()
  91. }
  92. return nil
  93. }(),
  94. "creem_products": setting.CreemProducts,
  95. "pay_methods": payMethods,
  96. "min_topup": operation_setting.MinTopUp,
  97. "stripe_min_topup": setting.StripeMinTopUp,
  98. "waffo_min_topup": setting.WaffoMinTopUp,
  99. "waffo_pancake_min_topup": setting.WaffoPancakeMinTopUp,
  100. "amount_options": operation_setting.GetPaymentSetting().AmountOptions,
  101. "discount": operation_setting.GetPaymentSetting().AmountDiscount,
  102. }
  103. common.ApiSuccess(c, data)
  104. }
  105. type EpayRequest struct {
  106. Amount int64 `json:"amount"`
  107. PaymentMethod string `json:"payment_method"`
  108. }
  109. type AmountRequest struct {
  110. Amount int64 `json:"amount"`
  111. }
  112. var nonEpayPaymentMethodsForCallback = []string{
  113. model.PaymentMethodStripe,
  114. model.PaymentMethodCreem,
  115. model.PaymentMethodWaffo,
  116. model.PaymentMethodWaffoPancake,
  117. }
  118. func isNonEpayPaymentMethodForEpayCallback(paymentMethod string) bool {
  119. return lo.Contains(nonEpayPaymentMethodsForCallback, paymentMethod)
  120. }
  121. func GetEpayClient() *epay.Client {
  122. if operation_setting.PayAddress == "" || operation_setting.EpayId == "" || operation_setting.EpayKey == "" {
  123. return nil
  124. }
  125. withUrl, err := epay.NewClient(&epay.Config{
  126. PartnerID: operation_setting.EpayId,
  127. Key: operation_setting.EpayKey,
  128. }, operation_setting.PayAddress)
  129. if err != nil {
  130. return nil
  131. }
  132. return withUrl
  133. }
  134. func getPayMoney(amount int64, group string) float64 {
  135. dAmount := decimal.NewFromInt(amount)
  136. // 充值金额以“展示类型”为准:
  137. // - USD/CNY: 前端传 amount 为金额单位;TOKENS: 前端传 tokens,需要换成 USD 金额
  138. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  139. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  140. dAmount = dAmount.Div(dQuotaPerUnit)
  141. }
  142. topupGroupRatio := common.GetTopupGroupRatio(group)
  143. if topupGroupRatio == 0 {
  144. topupGroupRatio = 1
  145. }
  146. dTopupGroupRatio := decimal.NewFromFloat(topupGroupRatio)
  147. dPrice := decimal.NewFromFloat(operation_setting.Price)
  148. // apply optional preset discount by the original request amount (if configured), default 1.0
  149. discount := 1.0
  150. if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(amount)]; ok {
  151. if ds > 0 {
  152. discount = ds
  153. }
  154. }
  155. dDiscount := decimal.NewFromFloat(discount)
  156. payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio).Mul(dDiscount)
  157. return payMoney.InexactFloat64()
  158. }
  159. func getMinTopup() int64 {
  160. minTopup := operation_setting.MinTopUp
  161. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  162. dMinTopup := decimal.NewFromInt(int64(minTopup))
  163. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  164. minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
  165. }
  166. return int64(minTopup)
  167. }
  168. func RequestEpay(c *gin.Context) {
  169. var req EpayRequest
  170. err := c.ShouldBindJSON(&req)
  171. if err != nil {
  172. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "参数错误"})
  173. return
  174. }
  175. if req.Amount < getMinTopup() {
  176. c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
  177. return
  178. }
  179. id := c.GetInt("id")
  180. group, err := model.GetUserGroup(id, true)
  181. if err != nil {
  182. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
  183. return
  184. }
  185. payMoney := getPayMoney(req.Amount, group)
  186. if payMoney < 0.01 {
  187. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "充值金额过低"})
  188. return
  189. }
  190. if !operation_setting.ContainsPayMethod(req.PaymentMethod) {
  191. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "支付方式不存在"})
  192. return
  193. }
  194. callBackAddress := service.GetCallbackAddress()
  195. returnUrl, _ := url.Parse(system_setting.ServerAddress + "/console/log")
  196. notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
  197. tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
  198. tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
  199. client := GetEpayClient()
  200. if client == nil {
  201. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "当前管理员未配置支付信息"})
  202. return
  203. }
  204. uri, params, err := client.Purchase(&epay.PurchaseArgs{
  205. Type: req.PaymentMethod,
  206. ServiceTradeNo: tradeNo,
  207. Name: fmt.Sprintf("TUC%d", req.Amount),
  208. Money: strconv.FormatFloat(payMoney, 'f', 2, 64),
  209. Device: epay.PC,
  210. NotifyUrl: notifyUrl,
  211. ReturnUrl: returnUrl,
  212. })
  213. if err != nil {
  214. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 拉起支付失败 user_id=%d trade_no=%s payment_method=%s amount=%d error=%q", id, tradeNo, req.PaymentMethod, req.Amount, err.Error()))
  215. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "拉起支付失败"})
  216. return
  217. }
  218. amount := req.Amount
  219. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  220. dAmount := decimal.NewFromInt(int64(amount))
  221. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  222. amount = dAmount.Div(dQuotaPerUnit).IntPart()
  223. }
  224. topUp := &model.TopUp{
  225. UserId: id,
  226. Amount: amount,
  227. Money: payMoney,
  228. TradeNo: tradeNo,
  229. PaymentMethod: req.PaymentMethod,
  230. CreateTime: time.Now().Unix(),
  231. Status: common.TopUpStatusPending,
  232. }
  233. err = topUp.Insert()
  234. if err != nil {
  235. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 创建充值订单失败 user_id=%d trade_no=%s payment_method=%s amount=%d error=%q", id, tradeNo, req.PaymentMethod, req.Amount, err.Error()))
  236. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "创建订单失败"})
  237. return
  238. }
  239. logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 充值订单创建成功 user_id=%d trade_no=%s payment_method=%s amount=%d money=%.2f uri=%q params=%q", id, tradeNo, req.PaymentMethod, req.Amount, payMoney, uri, common.GetJsonString(params)))
  240. c.JSON(http.StatusOK, gin.H{"message": "success", "data": params, "url": uri})
  241. }
  242. // tradeNo lock
  243. var orderLocks sync.Map
  244. var createLock sync.Mutex
  245. // refCountedMutex 带引用计数的互斥锁,确保最后一个使用者才从 map 中删除
  246. type refCountedMutex struct {
  247. mu sync.Mutex
  248. refCount int
  249. }
  250. // LockOrder 尝试对给定订单号加锁
  251. func LockOrder(tradeNo string) {
  252. createLock.Lock()
  253. var rcm *refCountedMutex
  254. if v, ok := orderLocks.Load(tradeNo); ok {
  255. rcm = v.(*refCountedMutex)
  256. } else {
  257. rcm = &refCountedMutex{}
  258. orderLocks.Store(tradeNo, rcm)
  259. }
  260. rcm.refCount++
  261. createLock.Unlock()
  262. rcm.mu.Lock()
  263. }
  264. // UnlockOrder 释放给定订单号的锁
  265. func UnlockOrder(tradeNo string) {
  266. v, ok := orderLocks.Load(tradeNo)
  267. if !ok {
  268. return
  269. }
  270. rcm := v.(*refCountedMutex)
  271. rcm.mu.Unlock()
  272. createLock.Lock()
  273. rcm.refCount--
  274. if rcm.refCount == 0 {
  275. orderLocks.Delete(tradeNo)
  276. }
  277. createLock.Unlock()
  278. }
  279. func EpayNotify(c *gin.Context) {
  280. if !isEpayWebhookEnabled() {
  281. logger.LogWarn(c.Request.Context(), fmt.Sprintf("易支付 webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s", c.Request.RequestURI, c.ClientIP()))
  282. _, _ = c.Writer.Write([]byte("fail"))
  283. return
  284. }
  285. var params map[string]string
  286. if c.Request.Method == "POST" {
  287. // POST 请求:从 POST body 解析参数
  288. if err := c.Request.ParseForm(); err != nil {
  289. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 webhook POST 表单解析失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
  290. _, _ = c.Writer.Write([]byte("fail"))
  291. return
  292. }
  293. params = lo.Reduce(lo.Keys(c.Request.PostForm), func(r map[string]string, t string, i int) map[string]string {
  294. r[t] = c.Request.PostForm.Get(t)
  295. return r
  296. }, map[string]string{})
  297. } else {
  298. // GET 请求:从 URL Query 解析参数
  299. params = lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
  300. r[t] = c.Request.URL.Query().Get(t)
  301. return r
  302. }, map[string]string{})
  303. }
  304. logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 webhook 收到请求 path=%q client_ip=%s method=%s params=%q", c.Request.RequestURI, c.ClientIP(), c.Request.Method, common.GetJsonString(params)))
  305. if len(params) == 0 {
  306. logger.LogWarn(c.Request.Context(), fmt.Sprintf("易支付 webhook 参数为空 path=%q client_ip=%s", c.Request.RequestURI, c.ClientIP()))
  307. _, _ = c.Writer.Write([]byte("fail"))
  308. return
  309. }
  310. client := GetEpayClient()
  311. if client == nil {
  312. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 client 未初始化 path=%q client_ip=%s", c.Request.RequestURI, c.ClientIP()))
  313. _, err := c.Writer.Write([]byte("fail"))
  314. if err != nil {
  315. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 webhook 响应写入失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
  316. }
  317. return
  318. }
  319. verifyInfo, err := client.Verify(params)
  320. if err == nil && verifyInfo.VerifyStatus {
  321. logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 webhook 验签成功 trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q", verifyInfo.ServiceTradeNo, verifyInfo.Type, verifyInfo.TradeStatus, c.ClientIP(), common.GetJsonString(verifyInfo)))
  322. _, err := c.Writer.Write([]byte("success"))
  323. if err != nil {
  324. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 webhook 响应写入失败 trade_no=%s client_ip=%s error=%q", verifyInfo.ServiceTradeNo, c.ClientIP(), err.Error()))
  325. }
  326. } else {
  327. _, err := c.Writer.Write([]byte("fail"))
  328. if err != nil {
  329. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 webhook 响应写入失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
  330. }
  331. if err != nil {
  332. logger.LogWarn(c.Request.Context(), fmt.Sprintf("易支付 webhook 验签失败 path=%q client_ip=%s verify_error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
  333. } else {
  334. logger.LogWarn(c.Request.Context(), fmt.Sprintf("易支付 webhook 验签失败 path=%q client_ip=%s verify_status=false", c.Request.RequestURI, c.ClientIP()))
  335. }
  336. return
  337. }
  338. if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
  339. LockOrder(verifyInfo.ServiceTradeNo)
  340. defer UnlockOrder(verifyInfo.ServiceTradeNo)
  341. topUp := model.GetTopUpByTradeNo(verifyInfo.ServiceTradeNo)
  342. if topUp == nil {
  343. logger.LogWarn(c.Request.Context(), fmt.Sprintf("易支付 回调订单不存在 trade_no=%s callback_type=%s client_ip=%s verify_info=%q", verifyInfo.ServiceTradeNo, verifyInfo.Type, c.ClientIP(), common.GetJsonString(verifyInfo)))
  344. return
  345. }
  346. if isNonEpayPaymentMethodForEpayCallback(topUp.PaymentMethod) {
  347. logger.LogWarn(c.Request.Context(), fmt.Sprintf("易支付 订单支付方式不匹配 trade_no=%s order_payment_method=%s callback_type=%s client_ip=%s", verifyInfo.ServiceTradeNo, topUp.PaymentMethod, verifyInfo.Type, c.ClientIP()))
  348. return
  349. }
  350. if topUp.PaymentMethod != verifyInfo.Type {
  351. logger.LogWarn(c.Request.Context(), fmt.Sprintf("易支付 订单支付方式不匹配 trade_no=%s order_payment_method=%s callback_type=%s client_ip=%s", verifyInfo.ServiceTradeNo, topUp.PaymentMethod, verifyInfo.Type, c.ClientIP()))
  352. return
  353. }
  354. if topUp.Status == common.TopUpStatusPending {
  355. topUp.Status = common.TopUpStatusSuccess
  356. err := topUp.Update()
  357. if err != nil {
  358. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 更新充值订单失败 trade_no=%s user_id=%d client_ip=%s error=%q topup=%q", topUp.TradeNo, topUp.UserId, c.ClientIP(), err.Error(), common.GetJsonString(topUp)))
  359. return
  360. }
  361. //user, _ := model.GetUserById(topUp.UserId, false)
  362. //user.Quota += topUp.Amount * 500000
  363. dAmount := decimal.NewFromInt(int64(topUp.Amount))
  364. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  365. quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart())
  366. err = model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true)
  367. if err != nil {
  368. logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 更新用户额度失败 trade_no=%s user_id=%d client_ip=%s quota_to_add=%d error=%q topup=%q", topUp.TradeNo, topUp.UserId, c.ClientIP(), quotaToAdd, err.Error(), common.GetJsonString(topUp)))
  369. return
  370. }
  371. logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 充值成功 trade_no=%s user_id=%d client_ip=%s quota_to_add=%d money=%.2f topup=%q", topUp.TradeNo, topUp.UserId, c.ClientIP(), quotaToAdd, topUp.Money, common.GetJsonString(topUp)))
  372. model.RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money), c.ClientIP(), topUp.PaymentMethod, "epay")
  373. }
  374. } else {
  375. logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 webhook 忽略事件 trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q", verifyInfo.ServiceTradeNo, verifyInfo.Type, verifyInfo.TradeStatus, c.ClientIP(), common.GetJsonString(verifyInfo)))
  376. }
  377. }
  378. func RequestAmount(c *gin.Context) {
  379. var req AmountRequest
  380. err := c.ShouldBindJSON(&req)
  381. if err != nil {
  382. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "参数错误"})
  383. return
  384. }
  385. if req.Amount < getMinTopup() {
  386. c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
  387. return
  388. }
  389. id := c.GetInt("id")
  390. group, err := model.GetUserGroup(id, true)
  391. if err != nil {
  392. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
  393. return
  394. }
  395. payMoney := getPayMoney(req.Amount, group)
  396. if payMoney <= 0.01 {
  397. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "充值金额过低"})
  398. return
  399. }
  400. c.JSON(http.StatusOK, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
  401. }
  402. func GetUserTopUps(c *gin.Context) {
  403. userId := c.GetInt("id")
  404. pageInfo := common.GetPageQuery(c)
  405. keyword := c.Query("keyword")
  406. var (
  407. topups []*model.TopUp
  408. total int64
  409. err error
  410. )
  411. if keyword != "" {
  412. topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo)
  413. } else {
  414. topups, total, err = model.GetUserTopUps(userId, pageInfo)
  415. }
  416. if err != nil {
  417. common.ApiError(c, err)
  418. return
  419. }
  420. pageInfo.SetTotal(int(total))
  421. pageInfo.SetItems(topups)
  422. common.ApiSuccess(c, pageInfo)
  423. }
  424. // GetAllTopUps 管理员获取全平台充值记录
  425. func GetAllTopUps(c *gin.Context) {
  426. pageInfo := common.GetPageQuery(c)
  427. keyword := c.Query("keyword")
  428. var (
  429. topups []*model.TopUp
  430. total int64
  431. err error
  432. )
  433. if keyword != "" {
  434. topups, total, err = model.SearchAllTopUps(keyword, pageInfo)
  435. } else {
  436. topups, total, err = model.GetAllTopUps(pageInfo)
  437. }
  438. if err != nil {
  439. common.ApiError(c, err)
  440. return
  441. }
  442. pageInfo.SetTotal(int(total))
  443. pageInfo.SetItems(topups)
  444. common.ApiSuccess(c, pageInfo)
  445. }
  446. type AdminCompleteTopupRequest struct {
  447. TradeNo string `json:"trade_no"`
  448. }
  449. // AdminCompleteTopUp 管理员补单接口
  450. func AdminCompleteTopUp(c *gin.Context) {
  451. var req AdminCompleteTopupRequest
  452. if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" {
  453. common.ApiErrorMsg(c, "参数错误")
  454. return
  455. }
  456. // 订单级互斥,防止并发补单
  457. LockOrder(req.TradeNo)
  458. defer UnlockOrder(req.TradeNo)
  459. if err := model.ManualCompleteTopUp(req.TradeNo, c.ClientIP()); err != nil {
  460. common.ApiError(c, err)
  461. return
  462. }
  463. common.ApiSuccess(c, nil)
  464. }