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.
 
 
 

281 line
7.1 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. CreateTime: time.Now().Unix(),
  159. Status: common.TopUpStatusPending,
  160. }
  161. if err := topUp.Insert(); err != nil {
  162. c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
  163. return
  164. }
  165. c.JSON(200, gin.H{
  166. "message": "success",
  167. "data": gin.H{
  168. "trade_no": tradeNo,
  169. "qr_code_url": qrCode,
  170. },
  171. })
  172. }
  173. // AlipayPayStatus 轮询支付宝支付订单状态
  174. func AlipayPayStatus(c *gin.Context) {
  175. tradeNo := c.Query("trade_no")
  176. if tradeNo == "" {
  177. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  178. return
  179. }
  180. topUp := model.GetTopUpByTradeNo(tradeNo)
  181. if topUp == nil {
  182. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  183. return
  184. }
  185. userId := c.GetInt("id")
  186. if topUp.UserId != userId {
  187. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  188. return
  189. }
  190. c.JSON(200, gin.H{
  191. "message": "success",
  192. "data": gin.H{
  193. "status": topUp.Status,
  194. "amount": topUp.Amount,
  195. },
  196. })
  197. }
  198. // AlipayPayWebhook 处理支付宝异步回调通知
  199. func AlipayPayWebhook(c *gin.Context) {
  200. notifyReq, err := alipay.ParseNotifyToBodyMap(c.Request)
  201. if err != nil {
  202. log.Printf("解析支付宝回调失败: %v", err)
  203. c.String(http.StatusBadRequest, "fail")
  204. return
  205. }
  206. ok, err := alipay.VerifySign(setting.AlipayPublicKey, notifyReq)
  207. if err != nil {
  208. log.Printf("支付宝回调验签失败: %v", err)
  209. c.String(http.StatusBadRequest, "fail")
  210. return
  211. }
  212. if !ok {
  213. log.Printf("支付宝回调验签不通过")
  214. c.String(http.StatusBadRequest, "fail")
  215. return
  216. }
  217. tradeStatus := notifyReq.Get("trade_status")
  218. if tradeStatus != "TRADE_SUCCESS" {
  219. c.String(http.StatusOK, "success")
  220. return
  221. }
  222. tradeNo := notifyReq.Get("out_trade_no")
  223. LockOrder(tradeNo)
  224. defer UnlockOrder(tradeNo)
  225. if err := model.RechargeAlipay(tradeNo); err != nil {
  226. log.Printf("支付宝充值失败: %s, err: %s", tradeNo, err.Error())
  227. c.String(http.StatusInternalServerError, "fail")
  228. return
  229. }
  230. log.Printf("支付宝充值成功: %s", tradeNo)
  231. c.String(http.StatusOK, "success")
  232. }