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.
 
 
 

364 linhas
10 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. CreateTime: time.Now().Unix(),
  214. Status: common.TopUpStatusPending,
  215. }
  216. if err := topUp.Insert(); err != nil {
  217. c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
  218. return
  219. }
  220. c.JSON(200, gin.H{
  221. "message": "success",
  222. "data": gin.H{
  223. "trade_no": tradeNo,
  224. "qr_code_url": codeUrl,
  225. },
  226. })
  227. }
  228. // WechatPayStatus 轮询微信支付订单状态
  229. func WechatPayStatus(c *gin.Context) {
  230. tradeNo := c.Query("trade_no")
  231. if tradeNo == "" {
  232. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  233. return
  234. }
  235. topUp := model.GetTopUpByTradeNo(tradeNo)
  236. if topUp == nil {
  237. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  238. return
  239. }
  240. // 验证订单属于当前用户
  241. userId := c.GetInt("id")
  242. if topUp.UserId != userId {
  243. c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
  244. return
  245. }
  246. c.JSON(200, gin.H{
  247. "message": "success",
  248. "data": gin.H{
  249. "status": topUp.Status,
  250. "amount": topUp.Amount,
  251. },
  252. })
  253. }
  254. // WechatPayWebhook 处理微信支付回调通知
  255. func WechatPayWebhook(c *gin.Context) {
  256. notifyReq, err := wechat.V3ParseNotify(c.Request)
  257. if err != nil {
  258. log.Printf("解析微信支付回调失败: %v", err)
  259. c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "解析回调失败"})
  260. return
  261. }
  262. client, err := getWechatPayClient()
  263. if err != nil {
  264. log.Printf("获取微信支付客户端失败: %v", err)
  265. c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "服务错误"})
  266. return
  267. }
  268. if err := notifyReq.VerifySignByPK(client.WxPublicKey()); err != nil {
  269. log.Printf("微信支付回调验签失败: %v", err)
  270. c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "验签失败"})
  271. return
  272. }
  273. result, err := notifyReq.DecryptPayCipherText(setting.WechatPayAPIv3Key)
  274. if err != nil {
  275. log.Printf("解密微信支付回调数据失败: %v", err)
  276. c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "解密失败"})
  277. return
  278. }
  279. if result.TradeState != "SUCCESS" {
  280. log.Printf("微信支付回调非成功状态: %s, 订单号: %s", result.TradeState, result.OutTradeNo)
  281. c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
  282. return
  283. }
  284. tradeNo := result.OutTradeNo
  285. LockOrder(tradeNo)
  286. defer UnlockOrder(tradeNo)
  287. if err := model.CompleteSubscriptionOrder(tradeNo, common.GetJsonString(result)); err == nil {
  288. log.Printf("微信支付订阅订单完成: %s", tradeNo)
  289. c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
  290. return
  291. } else if !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
  292. log.Printf("微信支付订阅订单处理失败: %s, err: %s", tradeNo, err.Error())
  293. c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"})
  294. return
  295. }
  296. if err := model.RechargeWechat(tradeNo); err != nil {
  297. log.Printf("微信支付充值失败: %s, err: %s", tradeNo, err.Error())
  298. c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"})
  299. return
  300. }
  301. log.Printf("微信支付充值成功: %s, 金额: %d", tradeNo, result.Amount.Total)
  302. c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
  303. }
  304. func getWechatPayMoney(amount float64, group string) float64 {
  305. return calcPayMoney(amount, group, setting.WechatPayUnitPrice)
  306. }
  307. func getWechatMinTopup() int64 {
  308. return calcMinTopup(setting.WechatPayMinTopUp)
  309. }