194 lines
5.1 KiB
Go
194 lines
5.1 KiB
Go
package pay
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cloudprint/cloudprint-backend/config"
|
|
"github.com/iGoogle-ink/gopay"
|
|
"github.com/iGoogle-ink/gopay/wechat"
|
|
)
|
|
|
|
// 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 {
|
|
appID string
|
|
mchID string
|
|
apiKey string
|
|
}
|
|
|
|
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 {
|
|
return &WeChatPay{
|
|
appID: cfg.AppID,
|
|
mchID: cfg.MchID,
|
|
apiKey: cfg.ApiKey,
|
|
}
|
|
}
|
|
|
|
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.apiKey
|
|
|
|
md5Hash := md5.New()
|
|
md5Hash.Write([]byte(signStr))
|
|
return strings.ToUpper(hex.EncodeToString(md5Hash.Sum(nil)))
|
|
}
|
|
|
|
func (p *WeChatPay) UnifiedOrder(orderNo string, amount float64, description, openID, clientIP string) (*UnifiedOrderResponse, error) {
|
|
nonceStr := RandomString(32)
|
|
totalFee := int(amount * 100) // Convert to fen
|
|
|
|
// Build request body using gopay.BodyMap
|
|
body := gopay.BodyMap{}
|
|
body.Set("appid", p.appID)
|
|
body.Set("mch_id", p.mchID)
|
|
body.Set("nonce_str", nonceStr)
|
|
body.Set("body", description)
|
|
body.Set("out_trade_no", orderNo)
|
|
body.Set("total_fee", totalFee)
|
|
body.Set("spbill_create_ip", clientIP)
|
|
body.Set("trade_type", "JSAPI")
|
|
body.Set("openid", openID)
|
|
|
|
// Use go-pay client to call unified order
|
|
client := wechat.NewClient(p.mchID, p.apiKey, p.appID, false)
|
|
resp, err := client.UnifiedOrder(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unified order failed: %w", err)
|
|
}
|
|
|
|
result := &UnifiedOrderResponse{
|
|
ReturnCode: resp.ReturnCode,
|
|
ReturnMsg: resp.ReturnMsg,
|
|
ResultCode: resp.ResultCode,
|
|
PrepayID: resp.PrepayId,
|
|
CodeURL: resp.CodeUrl,
|
|
MWebURL: resp.MwebUrl,
|
|
ErrCode: resp.ErrCode,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (p *WeChatPay) QueryOrder(orderNo string) (string, string, error) {
|
|
nonceStr := RandomString(32)
|
|
|
|
body := gopay.BodyMap{}
|
|
body.Set("appid", p.appID)
|
|
body.Set("mch_id", p.mchID)
|
|
body.Set("nonce_str", nonceStr)
|
|
body.Set("out_trade_no", orderNo)
|
|
|
|
client := wechat.NewClient(p.mchID, p.apiKey, p.appID, false)
|
|
resp, _, err := client.QueryOrder(body)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return resp.TradeState, resp.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.appID,
|
|
"timeStamp": timestamp,
|
|
"nonceStr": nonceStr,
|
|
"package": "prepay_id=" + prepayID,
|
|
"signType": "MD5",
|
|
}
|
|
|
|
paySign := p.createSign(map[string]string{
|
|
"appId": p.appID,
|
|
"timeStamp": timestamp,
|
|
"nonceStr": nonceStr,
|
|
"package": "prepay_id=" + prepayID,
|
|
"signType": "MD5",
|
|
})
|
|
|
|
params["paySign"] = paySign
|
|
return params, nil
|
|
}
|