Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

288 rader
7.3 KiB

  1. package controller
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "strconv"
  9. "sync"
  10. "time"
  11. "github.com/QuantumNous/new-api/common"
  12. "github.com/QuantumNous/new-api/model"
  13. "github.com/QuantumNous/new-api/setting"
  14. "github.com/gin-gonic/gin"
  15. "github.com/go-pay/gopay"
  16. "github.com/go-pay/gopay/alipay"
  17. "github.com/thanhpk/randstr"
  18. )
  19. const (
  20. PaymentMethodAlipay = "alipay"
  21. )
  22. var alipayClientMu sync.Mutex
  23. var alipayClient *alipay.Client
  24. // ResetAlipayClient 重置支付宝客户端(配置变更时调用)
  25. func ResetAlipayClient() {
  26. alipayClientMu.Lock()
  27. alipayClient = nil
  28. alipayClientMu.Unlock()
  29. }
  30. func init() {
  31. setting.OnAlipayConfigChanged = ResetAlipayClient
  32. }
  33. // getAlipayClient 获取或创建支付宝客户端
  34. func getAlipayClient() (*alipay.Client, error) {
  35. alipayClientMu.Lock()
  36. defer alipayClientMu.Unlock()
  37. if alipayClient != nil {
  38. return alipayClient, nil
  39. }
  40. if !setting.IsAlipayConfigured() {
  41. return nil, fmt.Errorf("支付宝未配置")
  42. }
  43. client, err := alipay.NewClient(setting.AlipayAppID, setting.AlipayPrivateKey, true)
  44. if err != nil {
  45. return nil, fmt.Errorf("创建支付宝客户端失败: %w", err)
  46. }
  47. client.SetCharset("utf-8").
  48. SetSignType(alipay.RSA2).
  49. SetNotifyUrl(setting.AlipayNotifyURL)
  50. // 设置支付宝公钥(用于回调验签)
  51. pubKeyBytes, err := base64.StdEncoding.DecodeString(setting.AlipayPublicKey)
  52. if err != nil {
  53. pubKeyBytes = []byte(setting.AlipayPublicKey)
  54. }
  55. client.AutoVerifySign([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY")))
  56. alipayClient = client
  57. return alipayClient, nil
  58. }
  59. // AlipayPayRequest 支付宝支付请求参数
  60. type AlipayPayRequest struct {
  61. Amount int64 `json:"amount"`
  62. }
  63. // createAlipayPrecreateOrder 调用支付宝当面付预下单 API,返回二维码内容
  64. func createAlipayPrecreateOrder(client *alipay.Client, subject, tradeNo string, totalAmount string) (string, error) {
  65. bm := make(gopay.BodyMap)
  66. bm.Set("subject", subject).
  67. Set("out_trade_no", tradeNo).
  68. Set("total_amount", totalAmount).
  69. Set("product_code", "FACE_TO_FACE_PAYMENT")
  70. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  71. defer cancel()
  72. rsp, err := client.TradePrecreate(ctx, bm)
  73. if err != nil {
  74. return "", fmt.Errorf("支付宝当面付下单失败: %w", err)
  75. }
  76. if rsp.Response.Code != "10000" {
  77. return "", fmt.Errorf("支付宝错误: %s - %s", rsp.Response.Code, rsp.Response.Msg)
  78. }
  79. return rsp.Response.QrCode, nil
  80. }
  81. // RequestAlipayPayAmount 计算支付宝应付金额
  82. func RequestAlipayPayAmount(c *gin.Context) {
  83. var req AlipayPayRequest
  84. if err := c.ShouldBindJSON(&req); err != nil {
  85. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  86. return
  87. }
  88. minTopup := calcMinTopup(setting.AlipayMinTopUp)
  89. if req.Amount < minTopup {
  90. c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)})
  91. return
  92. }
  93. id := c.GetInt("id")
  94. group, err := model.GetUserGroup(id, true)
  95. if err != nil {
  96. c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
  97. return
  98. }
  99. payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice)
  100. if payMoney <= 0.01 {
  101. c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
  102. return
  103. }
  104. c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
  105. }
  106. // RequestAlipayPay 创建支付宝支付订单,返回二维码 URL
  107. func RequestAlipayPay(c *gin.Context) {
  108. var req AlipayPayRequest
  109. if err := c.ShouldBindJSON(&req); err != nil {
  110. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  111. return
  112. }
  113. minTopup := calcMinTopup(setting.AlipayMinTopUp)
  114. if req.Amount < minTopup {
  115. c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)})
  116. return
  117. }
  118. if req.Amount > 10000 {
  119. c.JSON(200, gin.H{"message": "error", "data": "充值数量不能大于 10000"})
  120. return
  121. }
  122. if !setting.IsAlipayConfigured() {
  123. c.JSON(200, gin.H{"message": "error", "data": "支付宝未配置"})
  124. return
  125. }
  126. client, err := getAlipayClient()
  127. if err != nil {
  128. log.Println("获取支付宝客户端失败:", err)
  129. c.JSON(200, gin.H{"message": "error", "data": "支付宝配置错误"})
  130. return
  131. }
  132. id := c.GetInt("id")
  133. group, err := model.GetUserGroup(id, true)
  134. if err != nil {
  135. c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
  136. return
  137. }
  138. topupGroupRatio := common.GetTopupGroupRatio(group)
  139. if topupGroupRatio == 0 {
  140. topupGroupRatio = 1
  141. }
  142. chargedMoney := float64(req.Amount) * topupGroupRatio
  143. tradeNo := fmt.Sprintf("ali%d%s", time.Now().UnixMilli(), randstr.String(8))
  144. payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice)
  145. totalAmount := strconv.FormatFloat(payMoney, 'f', 2, 64)
  146. qrCode, err := createAlipayPrecreateOrder(client, fmt.Sprintf("充值%d", req.Amount), tradeNo, totalAmount)
  147. if err != nil {
  148. log.Println(err)
  149. c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
  150. return
  151. }
  152. topUp := &model.TopUp{
  153. UserId: id,
  154. Amount: req.Amount,
  155. Money: chargedMoney,
  156. TradeNo: tradeNo,
  157. PaymentMethod: PaymentMethodAlipay,
  158. PaymentProvider: model.PaymentProviderAlipay,
  159. CreateTime: time.Now().Unix(),
  160. Status: common.TopUpStatusPending,
  161. }
  162. if err := topUp.Insert(); err != nil {
  163. c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
  164. return
  165. }
  166. c.JSON(200, gin.H{
  167. "message": "success",
  168. "data": gin.H{
  169. "trade_no": tradeNo,
  170. "qr_code_url": qrCode,
  171. },
  172. })
  173. }
  174. // AlipayPayStatus 轮询支付宝支付订单状态
  175. func AlipayPayStatus(c *gin.Context) {
  176. tradeNo := c.Query("trade_no")
  177. if tradeNo == "" {
  178. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  179. return
  180. }
  181. topUp := model.GetTopUpByTradeNo(tradeNo)
  182. if topUp == nil {
  183. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  184. return
  185. }
  186. userId := c.GetInt("id")
  187. if topUp.UserId != userId {
  188. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  189. return
  190. }
  191. c.JSON(200, gin.H{
  192. "message": "success",
  193. "data": gin.H{
  194. "status": topUp.Status,
  195. "amount": topUp.Amount,
  196. },
  197. })
  198. }
  199. // AlipayPayWebhook 处理支付宝异步回调通知
  200. func AlipayPayWebhook(c *gin.Context) {
  201. if !isAlipayWebhookEnabled() {
  202. log.Printf("支付宝 webhook 被拒绝: 支付宝未配置 (client_ip=%s)\n", c.ClientIP())
  203. c.String(http.StatusForbidden, "fail")
  204. return
  205. }
  206. notifyReq, err := alipay.ParseNotifyToBodyMap(c.Request)
  207. if err != nil {
  208. log.Printf("解析支付宝回调失败: %v", err)
  209. c.String(http.StatusBadRequest, "fail")
  210. return
  211. }
  212. ok, err := alipay.VerifySign(setting.AlipayPublicKey, notifyReq)
  213. if err != nil {
  214. log.Printf("支付宝回调验签失败: %v", err)
  215. c.String(http.StatusBadRequest, "fail")
  216. return
  217. }
  218. if !ok {
  219. log.Printf("支付宝回调验签不通过")
  220. c.String(http.StatusBadRequest, "fail")
  221. return
  222. }
  223. tradeStatus := notifyReq.Get("trade_status")
  224. if tradeStatus != "TRADE_SUCCESS" {
  225. c.String(http.StatusOK, "success")
  226. return
  227. }
  228. tradeNo := notifyReq.Get("out_trade_no")
  229. LockOrder(tradeNo)
  230. defer UnlockOrder(tradeNo)
  231. if err := model.RechargeAlipay(tradeNo); err != nil {
  232. log.Printf("支付宝充值失败: %s, err: %s", tradeNo, err.Error())
  233. c.String(http.StatusInternalServerError, "fail")
  234. return
  235. }
  236. log.Printf("支付宝充值成功: %s", tradeNo)
  237. c.String(http.StatusOK, "success")
  238. }