Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

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