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.
 
 
 

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