516 lines
15 KiB
Go
516 lines
15 KiB
Go
package controller
|
||
|
||
import (
|
||
"crypto"
|
||
"crypto/aes"
|
||
"crypto/cipher"
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"encoding/base64"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/QuantumNous/new-api/common"
|
||
"github.com/QuantumNous/new-api/logger"
|
||
"github.com/QuantumNous/new-api/model"
|
||
"github.com/QuantumNous/new-api/service"
|
||
"github.com/QuantumNous/new-api/setting"
|
||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/shopspring/decimal"
|
||
)
|
||
|
||
const (
|
||
PaymentMethodWechatPay = "wxpay"
|
||
)
|
||
|
||
var wechatPayAdaptor = &WechatPayAdaptor{}
|
||
|
||
// WechatPayRequest represents a payment request for WeChat Pay.
|
||
type WechatPayRequest struct {
|
||
Amount int64 `json:"amount"`
|
||
PaymentMethod string `json:"payment_method"`
|
||
}
|
||
|
||
type WechatPayAdaptor struct{}
|
||
|
||
func getWechatPayAppId() string {
|
||
return setting.WechatPayAppId
|
||
}
|
||
|
||
func getWechatPayMchId() string {
|
||
if setting.WechatPaySandbox && setting.WechatPaySandboxMchId != "" {
|
||
return setting.WechatPaySandboxMchId
|
||
}
|
||
return setting.WechatPayMchId
|
||
}
|
||
|
||
func getWechatPayApiV3Key() string {
|
||
if setting.WechatPaySandbox && setting.WechatPaySandboxApiV3Key != "" {
|
||
return setting.WechatPaySandboxApiV3Key
|
||
}
|
||
return setting.WechatPayApiV3Key
|
||
}
|
||
|
||
func getWechatPayPrivateKey() string {
|
||
if setting.WechatPaySandbox && setting.WechatPaySandboxPrivateKey != "" {
|
||
return setting.WechatPaySandboxPrivateKey
|
||
}
|
||
return setting.WechatPayPrivateKey
|
||
}
|
||
|
||
func getWechatPayAPIBaseURL() string {
|
||
if setting.WechatPaySandbox {
|
||
return "https://apihk.mch.weixin.qq.com/sandboxnew"
|
||
}
|
||
return "https://api.mch.weixin.qq.com"
|
||
}
|
||
|
||
func getWechatPayMinTopUp() int64 {
|
||
minTopup := setting.WechatPayMinTopUp
|
||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||
minTopup = minTopup * int(common.QuotaPerUnit)
|
||
}
|
||
return int64(minTopup)
|
||
}
|
||
|
||
func getWechatPayPayMoney(amount float64, group string) float64 {
|
||
originalAmount := amount
|
||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||
amount = amount / common.QuotaPerUnit
|
||
}
|
||
topupGroupRatio := common.GetTopupGroupRatio(group)
|
||
if topupGroupRatio == 0 {
|
||
topupGroupRatio = 1
|
||
}
|
||
discount := 1.0
|
||
if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok {
|
||
if ds > 0 {
|
||
discount = ds
|
||
}
|
||
}
|
||
return amount * setting.WechatPayUnitPrice * topupGroupRatio * discount
|
||
}
|
||
|
||
// wechatPaySign generates the WeChat Pay v3 signature
|
||
func wechatPaySign(method string, urlPath string, body string, timestamp string, nonce string, privateKeyStr string) (string, error) {
|
||
message := fmt.Sprintf("%s\n%s\n%s\n%s\n", method, urlPath, timestamp, nonce)
|
||
if body != "" && body != "{}" {
|
||
message = fmt.Sprintf("%s\n%s\n%s\n%s\n", method, urlPath, timestamp, nonce) + body
|
||
} else {
|
||
message = fmt.Sprintf("%s\n%s\n%s\n%s\n", method, urlPath, timestamp, nonce)
|
||
}
|
||
|
||
block, _ := pem.Decode([]byte(privateKeyStr))
|
||
if block == nil {
|
||
return "", fmt.Errorf("failed to decode private key PEM")
|
||
}
|
||
|
||
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||
if err != nil {
|
||
priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to parse private key: %v", err)
|
||
}
|
||
}
|
||
|
||
rsaKey, ok := priv.(*rsa.PrivateKey)
|
||
if !ok {
|
||
return "", fmt.Errorf("private key is not RSA key")
|
||
}
|
||
|
||
hashed := sha256.Sum256([]byte(message))
|
||
signature, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, hashed[:])
|
||
if err != nil {
|
||
return "", fmt.Errorf("sign failed: %v", err)
|
||
}
|
||
|
||
return base64.StdEncoding.EncodeToString(signature), nil
|
||
}
|
||
|
||
// wechatPayBuildAuthHeader builds the WeChat Pay v3 Authorization header
|
||
func wechatPayBuildAuthHeader(method string, urlPath string, body string) (string, error) {
|
||
mchId := getWechatPayMchId()
|
||
privateKey := getWechatPayPrivateKey()
|
||
certSerial := setting.WechatPayCertSerial
|
||
|
||
if mchId == "" || privateKey == "" || certSerial == "" {
|
||
return "", fmt.Errorf("WeChat Pay not fully configured")
|
||
}
|
||
|
||
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||
nonce := common.GetRandomString(32)
|
||
|
||
sign, err := wechatPaySign(method, urlPath, body, timestamp, nonce, privateKey)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
auth := fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"`,
|
||
mchId, nonce, timestamp, certSerial, sign)
|
||
|
||
return auth, nil
|
||
}
|
||
|
||
// wechatPayPost sends a POST request to WeChat Pay API v3
|
||
func wechatPayPost(path string, requestBody interface{}) (map[string]interface{}, error) {
|
||
jsonBytes, err := common.Marshal(requestBody)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("marshal request body failed: %v", err)
|
||
}
|
||
|
||
bodyStr := string(jsonBytes)
|
||
authHeader, err := wechatPayBuildAuthHeader("POST", path, bodyStr)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
apiBaseURL := getWechatPayAPIBaseURL()
|
||
req, err := http.NewRequest("POST", apiBaseURL+path, strings.NewReader(bodyStr))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create request failed: %v", err)
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Authorization", authHeader)
|
||
req.Header.Set("Accept", "application/json")
|
||
req.Header.Set("User-Agent", "TokenFactory")
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("request failed: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read response failed: %v", err)
|
||
}
|
||
|
||
if resp.StatusCode != 200 && resp.StatusCode != 201 && resp.StatusCode != 204 {
|
||
return nil, fmt.Errorf("WeChat Pay API error: HTTP %d, body: %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
var result map[string]interface{}
|
||
if len(respBody) > 0 {
|
||
if err := common.Unmarshal(respBody, &result); err != nil {
|
||
return nil, fmt.Errorf("parse response failed: %v", err)
|
||
}
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// wechatPayDecryptResource decrypts WeChat Pay notification resource
|
||
func wechatPayDecryptResource(ciphertext, associatedData, nonce, apiV3Key string) ([]byte, error) {
|
||
aesKey, err := base64.StdEncoding.DecodeString(apiV3Key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("decode api v3 key failed: %v", err)
|
||
}
|
||
|
||
cipherData, err := base64.StdEncoding.DecodeString(ciphertext)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("decode ciphertext failed: %v", err)
|
||
}
|
||
|
||
nonceBytes := []byte(nonce)
|
||
adBytes := []byte(associatedData)
|
||
|
||
block, err := aes.NewCipher(aesKey)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create cipher failed: %v", err)
|
||
}
|
||
|
||
aesGCM, err := cipher.NewGCM(block)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create GCM failed: %v", err)
|
||
}
|
||
|
||
plaintext, err := aesGCM.Open(nil, nonceBytes, cipherData, adBytes)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("decrypt failed: %v", err)
|
||
}
|
||
|
||
return plaintext, nil
|
||
}
|
||
|
||
func (a *WechatPayAdaptor) RequestPay(c *gin.Context, req *WechatPayRequest) {
|
||
if req.PaymentMethod != PaymentMethodWechatPay {
|
||
c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"})
|
||
return
|
||
}
|
||
|
||
minTopup := getWechatPayMinTopUp()
|
||
if req.Amount < minTopup {
|
||
c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)})
|
||
return
|
||
}
|
||
if req.Amount > 100000 {
|
||
c.JSON(200, gin.H{"message": "error", "data": "充值数量不能大于 100000"})
|
||
return
|
||
}
|
||
|
||
id := c.GetInt("id")
|
||
user, err := model.GetUserById(id, false)
|
||
if err != nil || user == nil {
|
||
c.JSON(200, gin.H{"message": "error", "data": "用户不存在"})
|
||
return
|
||
}
|
||
|
||
group, err := model.GetUserGroup(id, true)
|
||
if err != nil {
|
||
c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||
return
|
||
}
|
||
|
||
payMoney := getWechatPayPayMoney(float64(req.Amount), group)
|
||
if payMoney <= 0.01 {
|
||
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
|
||
return
|
||
}
|
||
|
||
tradeNo := fmt.Sprintf("WX%d%s%d", id, common.GetRandomString(12), time.Now().Unix())
|
||
|
||
topUp := &model.TopUp{
|
||
UserId: id,
|
||
Amount: req.Amount,
|
||
Money: payMoney,
|
||
TradeNo: tradeNo,
|
||
PaymentMethod: PaymentMethodWechatPay,
|
||
CreateTime: time.Now().Unix(),
|
||
Status: common.TopUpStatusPending,
|
||
}
|
||
err = topUp.Insert()
|
||
if err != nil {
|
||
log.Printf("创建WeChat Pay订单失败: %v", err)
|
||
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
|
||
return
|
||
}
|
||
|
||
// Create WeChat Pay order via API v3
|
||
callBackAddress := service.GetCallbackAddress()
|
||
notifyUrl := strings.TrimRight(callBackAddress, "/") + "/api/wechatpay/notify"
|
||
if setting.WechatPayNotifyUrl != "" {
|
||
notifyUrl = setting.WechatPayNotifyUrl
|
||
}
|
||
|
||
amountFen := int64(payMoney * 100) // Convert yuan to fen
|
||
|
||
log.Printf("WeChat Pay创建订单 - 用户: %d, 订单: %s, 金额: %.2f元 (%d分)", id, tradeNo, payMoney, amountFen)
|
||
|
||
payParams := map[string]interface{}{
|
||
"mchid": getWechatPayMchId(),
|
||
"out_trade_no": tradeNo,
|
||
"appid": getWechatPayAppId(),
|
||
"description": "TokenFactory TopUp",
|
||
"notify_url": notifyUrl,
|
||
"amount": map[string]interface{}{
|
||
"total": amountFen,
|
||
"currency": "CNY",
|
||
},
|
||
"payer": map[string]interface{}{
|
||
"openid": "", // Will be obtained from WeChat OAuth
|
||
},
|
||
"scene_info": map[string]interface{}{
|
||
"payer_client_ip": c.ClientIP(),
|
||
},
|
||
}
|
||
|
||
// For H5 payment
|
||
payParams["scene_info"].(map[string]interface{})["h5_info"] = map[string]interface{}{
|
||
"type": "Wap",
|
||
}
|
||
|
||
result, err := wechatPayPost("/v3/pay/transactions/h5", payParams)
|
||
if err != nil {
|
||
log.Printf("WeChat Pay创建订单失败: %v", err)
|
||
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
||
return
|
||
}
|
||
|
||
h5Url := ""
|
||
if result != nil {
|
||
if urlStr, ok := result["h5_url"].(string); ok {
|
||
h5Url = urlStr
|
||
}
|
||
}
|
||
|
||
log.Printf("WeChat Pay订单创建成功 - 用户ID: %d, 订单号: %s, 金额: %.2f", id, tradeNo, payMoney)
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "success",
|
||
"data": gin.H{
|
||
"pay_url": h5Url,
|
||
"trade_no": tradeNo,
|
||
},
|
||
})
|
||
}
|
||
|
||
// WechatPayNotify handles the WeChat Pay payment notification
|
||
func WechatPayNotify(c *gin.Context) {
|
||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||
if err != nil {
|
||
log.Printf("读取WeChat Pay通知失败: %v", err)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "read body failed"})
|
||
return
|
||
}
|
||
defer c.Request.Body.Close()
|
||
|
||
log.Printf("收到WeChat Pay通知: %s", string(bodyBytes))
|
||
|
||
var notifyData struct {
|
||
Id string `json:"id"`
|
||
CreateTime string `json:"create_time"`
|
||
EventType string `json:"event_type"`
|
||
ResourceType string `json:"resource_type"`
|
||
Resource struct {
|
||
Algorithm string `json:"algorithm"`
|
||
Ciphertext string `json:"ciphertext"`
|
||
AssociatedData string `json:"associated_data"`
|
||
Nonce string `json:"nonce"`
|
||
OriginalType string `json:"original_type"`
|
||
} `json:"resource"`
|
||
Summary string `json:"summary"`
|
||
}
|
||
|
||
if err := common.Unmarshal(bodyBytes, ¬ifyData); err != nil {
|
||
log.Printf("解析WeChat Pay通知失败: %v", err)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "parse failed"})
|
||
return
|
||
}
|
||
|
||
apiV3Key := getWechatPayApiV3Key()
|
||
if apiV3Key == "" {
|
||
log.Printf("WeChat Pay API v3 key not configured")
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "not configured"})
|
||
return
|
||
}
|
||
|
||
// Decrypt the resource
|
||
plaintext, err := wechatPayDecryptResource(
|
||
notifyData.Resource.Ciphertext,
|
||
notifyData.Resource.AssociatedData,
|
||
notifyData.Resource.Nonce,
|
||
apiV3Key,
|
||
)
|
||
if err != nil {
|
||
log.Printf("WeChat Pay通知解密失败: %v", err)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "decrypt failed"})
|
||
return
|
||
}
|
||
|
||
log.Printf("WeChat Pay通知解密成功: %s", string(plaintext))
|
||
|
||
var paymentResult struct {
|
||
AppId string `json:"appid"`
|
||
MchId string `json:"mchid"`
|
||
OutTradeNo string `json:"out_trade_no"`
|
||
TransactionId string `json:"transaction_id"`
|
||
TradeType string `json:"trade_type"`
|
||
TradeState string `json:"trade_state"`
|
||
TradeStateDesc string `json:"trade_state_desc"`
|
||
BankType string `json:"bank_type"`
|
||
Attach string `json:"attach"`
|
||
SuccessTime string `json:"success_time"`
|
||
Payer struct {
|
||
OpenId string `json:"openid"`
|
||
} `json:"payer"`
|
||
Amount struct {
|
||
Total int `json:"total"`
|
||
PayerTotal int `json:"payer_total"`
|
||
Currency string `json:"currency"`
|
||
PayerCurrency string `json:"payer_currency"`
|
||
} `json:"amount"`
|
||
}
|
||
|
||
if err := common.Unmarshal(plaintext, &paymentResult); err != nil {
|
||
log.Printf("解析WeChat Pay支付结果失败: %v", err)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "parse result failed"})
|
||
return
|
||
}
|
||
|
||
tradeNo := paymentResult.OutTradeNo
|
||
log.Printf("WeChat Pay支付结果: out_trade_no=%s, transaction_id=%s, trade_state=%s",
|
||
tradeNo, paymentResult.TransactionId, paymentResult.TradeState)
|
||
|
||
if paymentResult.TradeState != "SUCCESS" {
|
||
log.Printf("WeChat Pay支付未成功: %s, trade_state=%s", tradeNo, paymentResult.TradeState)
|
||
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "ok"})
|
||
return
|
||
}
|
||
|
||
LockOrder(tradeNo)
|
||
defer UnlockOrder(tradeNo)
|
||
|
||
topUp := model.GetTopUpByTradeNo(tradeNo)
|
||
if topUp == nil {
|
||
log.Printf("WeChat Pay通知未找到订单: %s", tradeNo)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "order not found"})
|
||
return
|
||
}
|
||
|
||
if topUp.Status != common.TopUpStatusPending {
|
||
log.Printf("WeChat Pay订单状态错误: %s, 当前状态: %s", tradeNo, topUp.Status)
|
||
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "ok"})
|
||
return
|
||
}
|
||
|
||
dAmount := decimal.NewFromInt(topUp.Amount)
|
||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||
quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart())
|
||
|
||
if quotaToAdd <= 0 {
|
||
log.Printf("WeChat Pay无效的充值额度: %s", tradeNo)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "invalid quota"})
|
||
return
|
||
}
|
||
|
||
topUp.CompleteTime = common.GetTimestamp()
|
||
topUp.Status = common.TopUpStatusSuccess
|
||
if err := topUp.Update(); err != nil {
|
||
log.Printf("WeChat Pay更新订单失败: %v", err)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "update failed"})
|
||
return
|
||
}
|
||
|
||
if err := model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true); err != nil {
|
||
log.Printf("WeChat Pay更新用户额度失败: %v", err)
|
||
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "quota update failed"})
|
||
return
|
||
}
|
||
|
||
model.RecordLog(topUp.UserId, model.LogTypeTopup,
|
||
fmt.Sprintf("使用微信支付充值成功,充值金额: %v,支付金额:%.2f",
|
||
logger.LogQuota(quotaToAdd), topUp.Money))
|
||
model.ApplyAffiliateTopupReward(topUp.UserId, quotaToAdd)
|
||
|
||
log.Printf("WeChat Pay充值成功 - 订单: %s, 额度: %d", tradeNo, quotaToAdd)
|
||
|
||
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "ok"})
|
||
}
|
||
|
||
// RequestWechatPay handles the WeChat Pay payment request from frontend
|
||
func RequestWechatPay(c *gin.Context) {
|
||
if !setting.WechatPayEnabled {
|
||
c.JSON(200, gin.H{"message": "error", "data": "微信支付未启用"})
|
||
return
|
||
}
|
||
|
||
var req WechatPayRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
|
||
return
|
||
}
|
||
|
||
wechatPayAdaptor.RequestPay(c, &req)
|
||
}
|