Files
backend/cloudprint-backend/pkg/pay/wechat.go

201 lines
5.4 KiB
Go

package pay
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/xml"
"fmt"
"math/rand"
"sort"
"strings"
"time"
"github.com/cloudprint/cloudprint-backend/config"
"github.com/iGoogle-ink/gopay"
)
// RandomString generates a random string with given length
func RandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
type WeChatPay struct {
client *gopay.WxPayClient
config *config.WeChatConfig
}
type UnifiedOrderResponse struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
ResultCode string `xml:"result_code"`
PrepayID string `xml:"prepay_id,omitempty"`
CodeURL string `xml:"code_url,omitempty"`
MWebURL string `xml:"mweb_url,omitempty"`
ErrCode string `xml:"err_code,omitempty"`
ErrMsg string `xml:"err_msg,omitempty"`
TransactionID string `xml:"transaction_id,omitempty"`
TradeState string `xml:"trade_state,omitempty"`
TotalFee int `xml:"total_fee,omitempty"`
}
type PayCallback struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
AppID string `xml:"appid"`
MchID string `xml:"mch_id"`
NonceStr string `xml:"nonce_str"`
Sign string `xml:"sign"`
ResultCode string `xml:"result_code"`
TransactionID string `xml:"transaction_id"`
OutTradeNo string `xml:"out_trade_no"`
TotalFee int `xml:"total_fee"`
TimeEnd string `xml:"time_end"`
}
func NewWeChatPay(cfg *config.WeChatConfig) *WeChatPay {
client := gopay.NewWxPayClient(cfg.AppID, cfg.MchID, cfg.ApiKey, cfg.NotifyURL)
if cfg.CertPath != "" {
client.SetCertFilePath(cfg.CertPath)
}
return &WeChatPay{
client: client,
config: cfg,
}
}
func (p *WeChatPay) createSign(params map[string]string) string {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var signParts []string
for _, k := range keys {
if params[k] != "" && k != "sign" {
signParts = append(signParts, fmt.Sprintf("%s=%s", k, params[k]))
}
}
signStr := strings.Join(signParts, "&")
signStr += "&key=" + p.config.ApiKey
md5Hash := md5.New()
md5Hash.Write([]byte(signStr))
return strings.ToUpper(hex.EncodeToString(md5Hash.Sum(nil)))
}
func (p *WeChatPay) UnifiedOrder(ctx context.Context, orderNo string, amount float64, description, openID, clientIP string) (*UnifiedOrderResponse, error) {
nonceStr := RandomString(32)
totalFee := int(amount * 100) // Convert to fen
// Build request body
body := gopay.UnifiedOrder{
AppID: p.config.AppID,
MchID: p.config.MchID,
NonceStr: nonceStr,
Body: description,
OutTradeNo: orderNo,
TotalFee: totalFee,
SpbillCreateIP: clientIP,
NotifyURL: p.config.NotifyURL,
TradeType: "JSAPI",
OpenID: openID,
}
// Use go-pay client to call unified order
resp, err := p.client.UnifiedOrder(ctx, body)
if err != nil {
return nil, fmt.Errorf("unified order failed: %w", err)
}
result := &UnifiedOrderResponse{
ReturnCode: resp.GetString("return_code"),
ReturnMsg: resp.GetString("return_msg"),
ResultCode: resp.GetString("result_code"),
PrepayID: resp.GetString("prepay_id"),
CodeURL: resp.GetString("code_url"),
MWebURL: resp.GetString("mweb_url"),
ErrCode: resp.GetString("err_code"),
ErrMsg: resp.GetString("err_msg"),
}
return result, nil
}
func (p *WeChatPay) QueryOrder(ctx context.Context, orderNo string) (string, int, error) {
nonceStr := RandomString(32)
body := gopay.QueryOrder{
AppID: p.config.AppID,
MchID: p.config.MchID,
NonceStr: nonceStr,
OutTradeNo: orderNo,
}
resp, err := p.client.QueryOrder(ctx, body)
if err != nil {
return "", 0, err
}
tradeState := resp.GetString("trade_state")
totalFee := resp.GetInt("total_fee")
return tradeState, totalFee, nil
}
func (p *WeChatPay) ParseCallback(body []byte) (*PayCallback, error) {
var callback PayCallback
if err := xml.Unmarshal(body, &callback); err != nil {
return nil, fmt.Errorf("failed to parse callback: %w", err)
}
return &callback, nil
}
func (p *WeChatPay) ValidateCallback(callback *PayCallback) bool {
params := map[string]string{
"return_code": callback.ReturnCode,
"return_msg": callback.ReturnMsg,
"appid": callback.AppID,
"mch_id": callback.MchID,
"nonce_str": callback.NonceStr,
"result_code": callback.ResultCode,
"transaction_id": callback.TransactionID,
"out_trade_no": callback.OutTradeNo,
"total_fee": fmt.Sprintf("%d", callback.TotalFee),
"time_end": callback.TimeEnd,
}
expectedSign := p.createSign(params)
return callback.Sign == expectedSign
}
func (p *WeChatPay) GenerateJSAPIPayParams(prepayID string) (map[string]string, error) {
nonceStr := RandomString(32)
timestamp := fmt.Sprintf("%d", time.Now().Unix())
params := map[string]string{
"appId": p.config.AppID,
"timeStamp": timestamp,
"nonceStr": nonceStr,
"package": "prepay_id=" + prepayID,
"signType": "MD5",
}
paySign := p.createSign(map[string]string{
"appId": p.config.AppID,
"timeStamp": timestamp,
"nonceStr": nonceStr,
"package": "prepay_id=" + prepayID,
"signType": "MD5",
})
params["paySign"] = paySign
return params, nil
}