Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

368 lignes
10 KiB

  1. package controller
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "errors"
  6. "fmt"
  7. "log"
  8. "math"
  9. "net/http"
  10. "os"
  11. "strconv"
  12. "sync"
  13. "time"
  14. "github.com/QuantumNous/new-api/common"
  15. "github.com/QuantumNous/new-api/model"
  16. "github.com/QuantumNous/new-api/setting"
  17. "github.com/QuantumNous/new-api/setting/operation_setting"
  18. "github.com/gin-gonic/gin"
  19. "github.com/go-pay/gopay"
  20. "github.com/go-pay/gopay/wechat/v3"
  21. "github.com/thanhpk/randstr"
  22. )
  23. const (
  24. PaymentMethodWechatPay = "wechat_pay"
  25. )
  26. var wechatPayClientMu sync.Mutex
  27. var wechatPayClient *wechat.ClientV3
  28. // ResetWechatPayClient 重置微信支付客户端(配置变更时调用)
  29. func ResetWechatPayClient() {
  30. wechatPayClientMu.Lock()
  31. wechatPayClient = nil
  32. wechatPayClientMu.Unlock()
  33. }
  34. func init() {
  35. setting.OnWechatPayConfigChanged = ResetWechatPayClient
  36. }
  37. // getWechatPayClient 获取或创建微信支付 V3 客户端
  38. func getWechatPayClient() (*wechat.ClientV3, error) {
  39. wechatPayClientMu.Lock()
  40. defer wechatPayClientMu.Unlock()
  41. if wechatPayClient != nil {
  42. return wechatPayClient, nil
  43. }
  44. if !setting.IsWechatPayConfigured() {
  45. return nil, fmt.Errorf("微信支付未配置")
  46. }
  47. var privateKey string
  48. if setting.WechatPayKeyB64 != "" {
  49. keyBytes, err := base64.StdEncoding.DecodeString(setting.WechatPayKeyB64)
  50. if err != nil {
  51. return nil, fmt.Errorf("解析商户私钥 Base64 失败: %w", err)
  52. }
  53. privateKey = string(keyBytes)
  54. } else {
  55. keyBytes, err := os.ReadFile(setting.WechatPayKeyPath)
  56. if err != nil {
  57. return nil, fmt.Errorf("读取商户私钥文件失败: %w", err)
  58. }
  59. privateKey = string(keyBytes)
  60. }
  61. client, err := wechat.NewClientV3(setting.WechatPayMchID, setting.WechatPaySerialNo, setting.WechatPayAPIv3Key, privateKey)
  62. if err != nil {
  63. return nil, fmt.Errorf("创建微信支付客户端失败: %w", err)
  64. }
  65. // 设置微信支付公钥(用于回调验签)
  66. if setting.WechatPayPubKeyB64 != "" {
  67. pubKeyBytes, err := base64.StdEncoding.DecodeString(setting.WechatPayPubKeyB64)
  68. if err != nil {
  69. return nil, fmt.Errorf("解析微信公钥 Base64 失败: %w", err)
  70. }
  71. if err := client.AutoVerifySignByPublicKey(pubKeyBytes, setting.WechatPayPubKeyID); err != nil {
  72. return nil, fmt.Errorf("开启公钥验签失败: %w", err)
  73. }
  74. } else {
  75. pubKeyBytes, err := os.ReadFile(setting.WechatPayPubKeyPath)
  76. if err != nil {
  77. return nil, fmt.Errorf("读取微信公钥文件失败: %w", err)
  78. }
  79. if err := client.AutoVerifySignByPublicKey(pubKeyBytes, setting.WechatPayPubKeyID); err != nil {
  80. return nil, fmt.Errorf("开启公钥验签失败: %w", err)
  81. }
  82. }
  83. wechatPayClient = client
  84. return wechatPayClient, nil
  85. }
  86. // WechatPayRequest 微信支付请求参数
  87. type WechatPayRequest struct {
  88. Amount int64 `json:"amount"`
  89. }
  90. // createWechatNativeOrder 调用微信 Native 下单 API,返回二维码 URL
  91. func createWechatNativeOrder(client *wechat.ClientV3, description, tradeNo string, totalFee int) (string, error) {
  92. bm := make(gopay.BodyMap)
  93. bm.Set("appid", setting.WechatPayAppID).
  94. Set("mchid", setting.WechatPayMchID).
  95. Set("description", description).
  96. Set("out_trade_no", tradeNo).
  97. Set("notify_url", setting.WechatPayNotifyURL).
  98. SetBodyMap("amount", func(bm gopay.BodyMap) {
  99. bm.Set("total", totalFee).
  100. Set("currency", "CNY")
  101. })
  102. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  103. defer cancel()
  104. rx, err := client.V3TransactionNative(ctx, bm)
  105. if err != nil {
  106. return "", fmt.Errorf("微信统一下单失败: %w", err)
  107. }
  108. if rx.Code != wechat.Success {
  109. return "", fmt.Errorf("微信支付错误: %d - %s", rx.Code, rx.ErrResponse.Message)
  110. }
  111. return rx.Response.CodeUrl, nil
  112. }
  113. // RequestWechatPayAmount 计算微信支付应付金额
  114. func RequestWechatPayAmount(c *gin.Context) {
  115. var req WechatPayRequest
  116. if err := c.ShouldBindJSON(&req); err != nil {
  117. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  118. return
  119. }
  120. minTopup := getWechatMinTopup()
  121. if req.Amount < minTopup {
  122. c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)})
  123. return
  124. }
  125. id := c.GetInt("id")
  126. group, err := model.GetUserGroup(id, true)
  127. if err != nil {
  128. c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
  129. return
  130. }
  131. payMoney := getWechatPayMoney(float64(req.Amount), group)
  132. if payMoney <= 0.01 {
  133. c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
  134. return
  135. }
  136. c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
  137. }
  138. // RequestWechatPay 创建微信支付订单,返回二维码 URL
  139. func RequestWechatPay(c *gin.Context) {
  140. var req WechatPayRequest
  141. if err := c.ShouldBindJSON(&req); err != nil {
  142. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  143. return
  144. }
  145. minTopup := getWechatMinTopup()
  146. if req.Amount < minTopup {
  147. c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)})
  148. return
  149. }
  150. if req.Amount > 10000 {
  151. c.JSON(200, gin.H{"message": "error", "data": "充值数量不能大于 10000"})
  152. return
  153. }
  154. if !setting.IsWechatPayConfigured() {
  155. c.JSON(200, gin.H{"message": "error", "data": "微信支付未配置"})
  156. return
  157. }
  158. client, err := getWechatPayClient()
  159. if err != nil {
  160. log.Println("获取微信支付客户端失败:", err)
  161. c.JSON(200, gin.H{"message": "error", "data": "微信支付配置错误"})
  162. return
  163. }
  164. id := c.GetInt("id")
  165. group, err := model.GetUserGroup(id, true)
  166. if err != nil {
  167. c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
  168. return
  169. }
  170. topupGroupRatio := common.GetTopupGroupRatio(group)
  171. if topupGroupRatio == 0 {
  172. topupGroupRatio = 1
  173. }
  174. chargedMoney := float64(req.Amount) * topupGroupRatio
  175. reference := fmt.Sprintf("wx-pay-%d-%d-%s", id, time.Now().UnixMilli(), randstr.String(4))
  176. tradeNo := "wx_" + common.Sha1([]byte(reference))
  177. payMoney := getWechatPayMoney(float64(req.Amount), group)
  178. totalFee := int(math.Round(payMoney * 100)) // 元 -> 分
  179. codeUrl, err := createWechatNativeOrder(client, fmt.Sprintf("充值%d", req.Amount), tradeNo, totalFee)
  180. if err != nil {
  181. log.Println(err)
  182. c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
  183. return
  184. }
  185. topUp := &model.TopUp{
  186. UserId: id,
  187. Amount: req.Amount,
  188. Money: chargedMoney,
  189. TradeNo: tradeNo,
  190. PaymentMethod: PaymentMethodWechatPay,
  191. CreateTime: time.Now().Unix(),
  192. Status: common.TopUpStatusPending,
  193. }
  194. if err := topUp.Insert(); err != nil {
  195. c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
  196. return
  197. }
  198. c.JSON(200, gin.H{
  199. "message": "success",
  200. "data": gin.H{
  201. "trade_no": tradeNo,
  202. "qr_code_url": codeUrl,
  203. },
  204. })
  205. }
  206. // WechatPayStatus 轮询微信支付订单状态
  207. func WechatPayStatus(c *gin.Context) {
  208. tradeNo := c.Query("trade_no")
  209. if tradeNo == "" {
  210. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  211. return
  212. }
  213. topUp := model.GetTopUpByTradeNo(tradeNo)
  214. if topUp == nil {
  215. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  216. return
  217. }
  218. // 验证订单属于当前用户
  219. userId := c.GetInt("id")
  220. if topUp.UserId != userId {
  221. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  222. return
  223. }
  224. c.JSON(200, gin.H{
  225. "message": "success",
  226. "data": gin.H{
  227. "status": topUp.Status,
  228. "amount": topUp.Amount,
  229. },
  230. })
  231. }
  232. // WechatPayWebhook 处理微信支付回调通知
  233. func WechatPayWebhook(c *gin.Context) {
  234. // 解析 V3 回调请求
  235. notifyReq, err := wechat.V3ParseNotify(c.Request)
  236. if err != nil {
  237. log.Printf("解析微信支付回调失败: %v", err)
  238. c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "解析回调失败"})
  239. return
  240. }
  241. client, err := getWechatPayClient()
  242. if err != nil {
  243. log.Printf("获取微信支付客户端失败: %v", err)
  244. c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "服务错误"})
  245. return
  246. }
  247. // 验证签名
  248. if err := notifyReq.VerifySignByPK(client.WxPublicKey()); err != nil {
  249. log.Printf("微信支付回调验签失败: %v", err)
  250. c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "验签失败"})
  251. return
  252. }
  253. // 解密回调数据
  254. result, err := notifyReq.DecryptPayCipherText(setting.WechatPayAPIv3Key)
  255. if err != nil {
  256. log.Printf("解密微信支付回调数据失败: %v", err)
  257. c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "解密失败"})
  258. return
  259. }
  260. // 只处理支付成功
  261. if result.TradeState != "SUCCESS" {
  262. log.Printf("微信支付回调非成功状态: %s, 订单号: %s", result.TradeState, result.OutTradeNo)
  263. c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
  264. return
  265. }
  266. tradeNo := result.OutTradeNo
  267. // 先尝试完成订阅订单
  268. LockOrder(tradeNo)
  269. defer UnlockOrder(tradeNo)
  270. if err := model.CompleteSubscriptionOrder(tradeNo, common.GetJsonString(result)); err == nil {
  271. log.Printf("微信支付订阅订单完成: %s", tradeNo)
  272. c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
  273. return
  274. } else if err != nil && !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
  275. log.Printf("微信支付订阅订单处理失败: %s, err: %s", tradeNo, err.Error())
  276. c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"})
  277. return
  278. }
  279. // 处理充值订单
  280. if err := model.RechargeWechat(tradeNo); err != nil {
  281. log.Printf("微信支付充值失败: %s, err: %s", tradeNo, err.Error())
  282. c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"})
  283. return
  284. }
  285. log.Printf("微信支付充值成功: %s, 金额: %d", tradeNo, result.Amount.Total)
  286. c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
  287. }
  288. // getWechatPayMoney 计算微信支付应付金额(元)
  289. func getWechatPayMoney(amount float64, group string) float64 {
  290. originalAmount := amount
  291. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  292. amount = amount / common.QuotaPerUnit
  293. }
  294. topupGroupRatio := common.GetTopupGroupRatio(group)
  295. if topupGroupRatio == 0 {
  296. topupGroupRatio = 1
  297. }
  298. discount := 1.0
  299. if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok {
  300. if ds > 0 {
  301. discount = ds
  302. }
  303. }
  304. payMoney := amount * setting.WechatPayUnitPrice * topupGroupRatio * discount
  305. return payMoney
  306. }
  307. // getWechatMinTopup 获取微信支付最低充值数量
  308. func getWechatMinTopup() int64 {
  309. minTopup := setting.WechatPayMinTopUp
  310. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  311. minTopup = minTopup * int(common.QuotaPerUnit)
  312. }
  313. return int64(minTopup)
  314. }