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.
 
 
 

371 linhas
11 KiB

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