Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

334 linhas
8.7 KiB

  1. package controller
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/pem"
  6. "fmt"
  7. "log"
  8. "net/http"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "time"
  13. "github.com/QuantumNous/new-api/common"
  14. "github.com/QuantumNous/new-api/model"
  15. "github.com/QuantumNous/new-api/setting"
  16. "github.com/QuantumNous/new-api/setting/operation_setting"
  17. "github.com/gin-gonic/gin"
  18. "github.com/go-pay/gopay"
  19. "github.com/go-pay/gopay/alipay"
  20. "github.com/thanhpk/randstr"
  21. )
  22. const (
  23. PaymentMethodAlipay = "alipay"
  24. )
  25. var alipayClientMu sync.Mutex
  26. var alipayClient *alipay.Client
  27. // ResetAlipayClient 重置支付宝客户端(配置变更时调用)
  28. func ResetAlipayClient() {
  29. alipayClientMu.Lock()
  30. alipayClient = nil
  31. alipayClientMu.Unlock()
  32. }
  33. func init() {
  34. setting.OnAlipayConfigChanged = ResetAlipayClient
  35. }
  36. // getAlipayClient 获取或创建支付宝客户端
  37. func getAlipayClient() (*alipay.Client, error) {
  38. alipayClientMu.Lock()
  39. defer alipayClientMu.Unlock()
  40. if alipayClient != nil {
  41. return alipayClient, nil
  42. }
  43. if !setting.IsAlipayConfigured() {
  44. return nil, fmt.Errorf("支付宝未配置")
  45. }
  46. client, err := alipay.NewClient(setting.AlipayAppID, setting.AlipayPrivateKey, true)
  47. if err != nil {
  48. return nil, fmt.Errorf("创建支付宝客户端失败: %w", err)
  49. }
  50. client.SetCharset("utf-8").
  51. SetSignType(alipay.RSA2).
  52. SetNotifyUrl(setting.AlipayNotifyURL)
  53. // 设置支付宝公钥(用于回调验签)
  54. pubKeyPEM := wrapAlipayPublicKey(setting.AlipayPublicKey)
  55. client.AutoVerifySign([]byte(pubKeyPEM))
  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 := getAlipayMinTopup()
  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 := getAlipayPayMoney(float64(req.Amount), group)
  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 := getAlipayMinTopup()
  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 := getAlipayPayMoney(float64(req.Amount), group)
  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. // 验证订单属于当前用户
  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. notifyReq, err := alipay.ParseNotifyToBodyMap(c.Request)
  202. if err != nil {
  203. log.Printf("解析支付宝回调失败: %v", err)
  204. c.String(http.StatusBadRequest, "fail")
  205. return
  206. }
  207. ok, err := alipay.VerifySign(setting.AlipayPublicKey, notifyReq)
  208. if err != nil {
  209. log.Printf("支付宝回调验签失败: %v", err)
  210. c.String(http.StatusBadRequest, "fail")
  211. return
  212. }
  213. if !ok {
  214. log.Printf("支付宝回调验签不通过")
  215. c.String(http.StatusBadRequest, "fail")
  216. return
  217. }
  218. tradeStatus := notifyReq.Get("trade_status")
  219. if tradeStatus != "TRADE_SUCCESS" {
  220. log.Printf("支付宝回调非成功状态: %s", tradeStatus)
  221. c.String(http.StatusOK, "success")
  222. return
  223. }
  224. tradeNo := notifyReq.Get("out_trade_no")
  225. LockOrder(tradeNo)
  226. defer UnlockOrder(tradeNo)
  227. if err := model.RechargeAlipay(tradeNo); err != nil {
  228. log.Printf("支付宝充值失败: %s, err: %s", tradeNo, err.Error())
  229. c.String(http.StatusInternalServerError, "fail")
  230. return
  231. }
  232. log.Printf("支付宝充值成功: %s", tradeNo)
  233. c.String(http.StatusOK, "success")
  234. }
  235. // getAlipayPayMoney 计算支付宝应付金额(元)
  236. func getAlipayPayMoney(amount float64, group string) float64 {
  237. originalAmount := amount
  238. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  239. amount = amount / common.QuotaPerUnit
  240. }
  241. topupGroupRatio := common.GetTopupGroupRatio(group)
  242. if topupGroupRatio == 0 {
  243. topupGroupRatio = 1
  244. }
  245. discount := 1.0
  246. if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok {
  247. if ds > 0 {
  248. discount = ds
  249. }
  250. }
  251. payMoney := amount * setting.AlipayUnitPrice * topupGroupRatio * discount
  252. return payMoney
  253. }
  254. // getAlipayMinTopup 获取支付宝最低充值数量
  255. func getAlipayMinTopup() int64 {
  256. minTopup := setting.AlipayMinTopUp
  257. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  258. minTopup = minTopup * int(common.QuotaPerUnit)
  259. }
  260. return int64(minTopup)
  261. }
  262. // wrapAlipayPublicKey 将支付宝公钥包装为 PEM 格式
  263. // 输入可能是:原始 Base64 字符串 或 已有 PEM 格式
  264. func wrapAlipayPublicKey(pubKey string) string {
  265. if strings.Contains(pubKey, "-----BEGIN") {
  266. return pubKey
  267. }
  268. // 去除空白字符
  269. cleaned := strings.ReplaceAll(pubKey, "\n", "")
  270. cleaned = strings.ReplaceAll(cleaned, "\r", "")
  271. cleaned = strings.TrimSpace(cleaned)
  272. // Base64 解码为 DER 字节
  273. derBytes, err := base64.StdEncoding.DecodeString(cleaned)
  274. if err != nil {
  275. // 如果解码失败,原样返回让上层报错
  276. return pubKey
  277. }
  278. return string(pem.EncodeToMemory(&pem.Block{
  279. Type: "PUBLIC KEY",
  280. Bytes: derBytes,
  281. }))
  282. }