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.
 
 
 

490 satır
14 KiB

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