You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

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