topup.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. package controller
  2. import (
  3. "fmt"
  4. "log"
  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 setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "" {
  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. data := gin.H{
  45. "enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
  46. "enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
  47. "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
  48. "creem_products": setting.CreemProducts,
  49. "pay_methods": payMethods,
  50. "min_topup": operation_setting.MinTopUp,
  51. "stripe_min_topup": setting.StripeMinTopUp,
  52. "amount_options": operation_setting.GetPaymentSetting().AmountOptions,
  53. "discount": operation_setting.GetPaymentSetting().AmountDiscount,
  54. }
  55. common.ApiSuccess(c, data)
  56. }
  57. type EpayRequest struct {
  58. Amount int64 `json:"amount"`
  59. PaymentMethod string `json:"payment_method"`
  60. TopUpCode string `json:"top_up_code"`
  61. }
  62. type AmountRequest struct {
  63. Amount int64 `json:"amount"`
  64. TopUpCode string `json:"top_up_code"`
  65. }
  66. func GetEpayClient() *epay.Client {
  67. if operation_setting.PayAddress == "" || operation_setting.EpayId == "" || operation_setting.EpayKey == "" {
  68. return nil
  69. }
  70. withUrl, err := epay.NewClient(&epay.Config{
  71. PartnerID: operation_setting.EpayId,
  72. Key: operation_setting.EpayKey,
  73. }, operation_setting.PayAddress)
  74. if err != nil {
  75. return nil
  76. }
  77. return withUrl
  78. }
  79. func getPayMoney(amount int64, group string) float64 {
  80. dAmount := decimal.NewFromInt(amount)
  81. // 充值金额以“展示类型”为准:
  82. // - USD/CNY: 前端传 amount 为金额单位;TOKENS: 前端传 tokens,需要换成 USD 金额
  83. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  84. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  85. dAmount = dAmount.Div(dQuotaPerUnit)
  86. }
  87. topupGroupRatio := common.GetTopupGroupRatio(group)
  88. if topupGroupRatio == 0 {
  89. topupGroupRatio = 1
  90. }
  91. dTopupGroupRatio := decimal.NewFromFloat(topupGroupRatio)
  92. dPrice := decimal.NewFromFloat(operation_setting.Price)
  93. // apply optional preset discount by the original request amount (if configured), default 1.0
  94. discount := 1.0
  95. if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(amount)]; ok {
  96. if ds > 0 {
  97. discount = ds
  98. }
  99. }
  100. dDiscount := decimal.NewFromFloat(discount)
  101. payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio).Mul(dDiscount)
  102. return payMoney.InexactFloat64()
  103. }
  104. func getMinTopup() int64 {
  105. minTopup := operation_setting.MinTopUp
  106. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  107. dMinTopup := decimal.NewFromInt(int64(minTopup))
  108. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  109. minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
  110. }
  111. return int64(minTopup)
  112. }
  113. func RequestEpay(c *gin.Context) {
  114. var req EpayRequest
  115. err := c.ShouldBindJSON(&req)
  116. if err != nil {
  117. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  118. return
  119. }
  120. if req.Amount < getMinTopup() {
  121. c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
  122. return
  123. }
  124. id := c.GetInt("id")
  125. group, err := model.GetUserGroup(id, true)
  126. if err != nil {
  127. c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
  128. return
  129. }
  130. payMoney := getPayMoney(req.Amount, group)
  131. if payMoney < 0.01 {
  132. c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
  133. return
  134. }
  135. if !operation_setting.ContainsPayMethod(req.PaymentMethod) {
  136. c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"})
  137. return
  138. }
  139. callBackAddress := service.GetCallbackAddress()
  140. returnUrl, _ := url.Parse(system_setting.ServerAddress + "/console/log")
  141. notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
  142. tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
  143. tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
  144. client := GetEpayClient()
  145. if client == nil {
  146. c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"})
  147. return
  148. }
  149. uri, params, err := client.Purchase(&epay.PurchaseArgs{
  150. Type: req.PaymentMethod,
  151. ServiceTradeNo: tradeNo,
  152. Name: fmt.Sprintf("TUC%d", req.Amount),
  153. Money: strconv.FormatFloat(payMoney, 'f', 2, 64),
  154. Device: epay.PC,
  155. NotifyUrl: notifyUrl,
  156. ReturnUrl: returnUrl,
  157. })
  158. if err != nil {
  159. c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
  160. return
  161. }
  162. amount := req.Amount
  163. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  164. dAmount := decimal.NewFromInt(int64(amount))
  165. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  166. amount = dAmount.Div(dQuotaPerUnit).IntPart()
  167. }
  168. topUp := &model.TopUp{
  169. UserId: id,
  170. Amount: amount,
  171. Money: payMoney,
  172. TradeNo: tradeNo,
  173. PaymentMethod: req.PaymentMethod,
  174. CreateTime: time.Now().Unix(),
  175. Status: "pending",
  176. }
  177. err = topUp.Insert()
  178. if err != nil {
  179. c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
  180. return
  181. }
  182. c.JSON(200, gin.H{"message": "success", "data": params, "url": uri})
  183. }
  184. // tradeNo lock
  185. var orderLocks sync.Map
  186. var createLock sync.Mutex
  187. // LockOrder 尝试对给定订单号加锁
  188. func LockOrder(tradeNo string) {
  189. lock, ok := orderLocks.Load(tradeNo)
  190. if !ok {
  191. createLock.Lock()
  192. defer createLock.Unlock()
  193. lock, ok = orderLocks.Load(tradeNo)
  194. if !ok {
  195. lock = new(sync.Mutex)
  196. orderLocks.Store(tradeNo, lock)
  197. }
  198. }
  199. lock.(*sync.Mutex).Lock()
  200. }
  201. // UnlockOrder 释放给定订单号的锁
  202. func UnlockOrder(tradeNo string) {
  203. lock, ok := orderLocks.Load(tradeNo)
  204. if ok {
  205. lock.(*sync.Mutex).Unlock()
  206. }
  207. }
  208. func EpayNotify(c *gin.Context) {
  209. params := lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
  210. r[t] = c.Request.URL.Query().Get(t)
  211. return r
  212. }, map[string]string{})
  213. client := GetEpayClient()
  214. if client == nil {
  215. log.Println("易支付回调失败 未找到配置信息")
  216. _, err := c.Writer.Write([]byte("fail"))
  217. if err != nil {
  218. log.Println("易支付回调写入失败")
  219. }
  220. return
  221. }
  222. verifyInfo, err := client.Verify(params)
  223. if err == nil && verifyInfo.VerifyStatus {
  224. _, err := c.Writer.Write([]byte("success"))
  225. if err != nil {
  226. log.Println("易支付回调写入失败")
  227. }
  228. } else {
  229. _, err := c.Writer.Write([]byte("fail"))
  230. if err != nil {
  231. log.Println("易支付回调写入失败")
  232. }
  233. log.Println("易支付回调签名验证失败")
  234. return
  235. }
  236. if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
  237. log.Println(verifyInfo)
  238. LockOrder(verifyInfo.ServiceTradeNo)
  239. defer UnlockOrder(verifyInfo.ServiceTradeNo)
  240. topUp := model.GetTopUpByTradeNo(verifyInfo.ServiceTradeNo)
  241. if topUp == nil {
  242. log.Printf("易支付回调未找到订单: %v", verifyInfo)
  243. return
  244. }
  245. if topUp.Status == "pending" {
  246. topUp.Status = "success"
  247. err := topUp.Update()
  248. if err != nil {
  249. log.Printf("易支付回调更新订单失败: %v", topUp)
  250. return
  251. }
  252. //user, _ := model.GetUserById(topUp.UserId, false)
  253. //user.Quota += topUp.Amount * 500000
  254. dAmount := decimal.NewFromInt(int64(topUp.Amount))
  255. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  256. quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart())
  257. err = model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true)
  258. if err != nil {
  259. log.Printf("易支付回调更新用户失败: %v", topUp)
  260. return
  261. }
  262. log.Printf("易支付回调更新用户成功 %v", topUp)
  263. model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money))
  264. }
  265. } else {
  266. log.Printf("易支付异常回调: %v", verifyInfo)
  267. }
  268. }
  269. func RequestAmount(c *gin.Context) {
  270. var req AmountRequest
  271. err := c.ShouldBindJSON(&req)
  272. if err != nil {
  273. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  274. return
  275. }
  276. if req.Amount < getMinTopup() {
  277. c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
  278. return
  279. }
  280. id := c.GetInt("id")
  281. group, err := model.GetUserGroup(id, true)
  282. if err != nil {
  283. c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
  284. return
  285. }
  286. payMoney := getPayMoney(req.Amount, group)
  287. if payMoney <= 0.01 {
  288. c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
  289. return
  290. }
  291. c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
  292. }
  293. func GetUserTopUps(c *gin.Context) {
  294. userId := c.GetInt("id")
  295. pageInfo := common.GetPageQuery(c)
  296. keyword := c.Query("keyword")
  297. var (
  298. topups []*model.TopUp
  299. total int64
  300. err error
  301. )
  302. if keyword != "" {
  303. topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo)
  304. } else {
  305. topups, total, err = model.GetUserTopUps(userId, pageInfo)
  306. }
  307. if err != nil {
  308. common.ApiError(c, err)
  309. return
  310. }
  311. pageInfo.SetTotal(int(total))
  312. pageInfo.SetItems(topups)
  313. common.ApiSuccess(c, pageInfo)
  314. }
  315. // GetAllTopUps 管理员获取全平台充值记录
  316. func GetAllTopUps(c *gin.Context) {
  317. pageInfo := common.GetPageQuery(c)
  318. keyword := c.Query("keyword")
  319. var (
  320. topups []*model.TopUp
  321. total int64
  322. err error
  323. )
  324. if keyword != "" {
  325. topups, total, err = model.SearchAllTopUps(keyword, pageInfo)
  326. } else {
  327. topups, total, err = model.GetAllTopUps(pageInfo)
  328. }
  329. if err != nil {
  330. common.ApiError(c, err)
  331. return
  332. }
  333. pageInfo.SetTotal(int(total))
  334. pageInfo.SetItems(topups)
  335. common.ApiSuccess(c, pageInfo)
  336. }
  337. type AdminCompleteTopupRequest struct {
  338. TradeNo string `json:"trade_no"`
  339. }
  340. // AdminCompleteTopUp 管理员补单接口
  341. func AdminCompleteTopUp(c *gin.Context) {
  342. var req AdminCompleteTopupRequest
  343. if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" {
  344. common.ApiErrorMsg(c, "参数错误")
  345. return
  346. }
  347. // 订单级互斥,防止并发补单
  348. LockOrder(req.TradeNo)
  349. defer UnlockOrder(req.TradeNo)
  350. if err := model.ManualCompleteTopUp(req.TradeNo); err != nil {
  351. common.ApiError(c, err)
  352. return
  353. }
  354. common.ApiSuccess(c, nil)
  355. }