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 }