package wechatpay import ( "crypto/md5" "crypto/rand" "encoding/hex" "encoding/xml" "fmt" "io" "net/http" "sort" "strconv" "strings" "time" ) // Config is WeChat JSAPI merchant config. type Config struct { AppID string MchID string APIKey string NotifyURL string } // Enabled reports merchant credentials present. func (c Config) Enabled() bool { return strings.TrimSpace(c.AppID) != "" && strings.TrimSpace(c.MchID) != "" && strings.TrimSpace(c.APIKey) != "" } // JSAPIParams is returned to uni.requestPayment. type JSAPIParams struct { TimeStamp string `json:"timeStamp"` NonceStr string `json:"nonceStr"` PackageStr string `json:"packageStr"` SignType string `json:"signType"` PaySign string `json:"paySign"` } // SignMD5 builds WeChat v2 sign. func SignMD5(params map[string]string, apiKey string) string { keys := make([]string, 0, len(params)) for k, v := range params { if k == "sign" || v == "" { continue } keys = append(keys, k) } sort.Strings(keys) var b strings.Builder for i, k := range keys { if i > 0 { b.WriteByte('&') } b.WriteString(k) b.WriteByte('=') b.WriteString(params[k]) } b.WriteString("&key=") b.WriteString(apiKey) sum := md5.Sum([]byte(b.String())) return strings.ToUpper(hex.EncodeToString(sum[:])) } // BuildJSAPI signs prepay_id for the miniprogram. func BuildJSAPI(appID, prepayID, apiKey string) JSAPIParams { ts := strconv.FormatInt(time.Now().Unix(), 10) nonce := nonceStr() pkg := "prepay_id=" + prepayID m := map[string]string{ "appId": appID, "timeStamp": ts, "nonceStr": nonce, "package": pkg, "signType": "MD5", } return JSAPIParams{ TimeStamp: ts, NonceStr: nonce, PackageStr: pkg, SignType: "MD5", PaySign: SignMD5(m, apiKey), } } // MockJSAPI is used when merchant keys are absent (dev). func MockJSAPI() JSAPIParams { return JSAPIParams{ TimeStamp: strconv.FormatInt(time.Now().Unix(), 10), NonceStr: nonceStr(), PackageStr: "prepay_id=mock", SignType: "MD5", PaySign: "MOCK", } } type unifiedXML struct { XMLName xml.Name `xml:"xml"` AppID string `xml:"appid"` MchID string `xml:"mch_id"` Nonce string `xml:"nonce_str"` Body string `xml:"body"` OutNo string `xml:"out_trade_no"` Fee string `xml:"total_fee"` IP string `xml:"spbill_create_ip"` Notify string `xml:"notify_url"` Trade string `xml:"trade_type"` OpenID string `xml:"openid"` Sign string `xml:"sign"` } type unifiedResp struct { ReturnCode string `xml:"return_code"` ResultCode string `xml:"result_code"` PrepayID string `xml:"prepay_id"` ErrCodeDes string `xml:"err_code_des"` ReturnMsg string `xml:"return_msg"` } // UnifiedOrder calls WeChat v2 JSAPI unifiedorder. func UnifiedOrder(cfg Config, openid, orderNo string, amountFen int) (JSAPIParams, error) { if !cfg.Enabled() { return MockJSAPI(), nil } nonce := nonceStr() params := map[string]string{ "appid": cfg.AppID, "mch_id": cfg.MchID, "nonce_str": nonce, "body": "课程预约", "out_trade_no": orderNo, "total_fee": strconv.Itoa(amountFen), "spbill_create_ip": "127.0.0.1", "notify_url": cfg.NotifyURL, "trade_type": "JSAPI", "openid": openid, } params["sign"] = SignMD5(params, cfg.APIKey) raw, err := xml.Marshal(unifiedXML{ AppID: cfg.AppID, MchID: cfg.MchID, Nonce: nonce, Body: "课程预约", OutNo: orderNo, Fee: params["total_fee"], IP: "127.0.0.1", Notify: cfg.NotifyURL, Trade: "JSAPI", OpenID: openid, Sign: params["sign"], }) if err != nil { return JSAPIParams{}, err } resp, err := http.Post("https://api.mch.weixin.qq.com/pay/unifiedorder", "application/xml", strings.NewReader(string(raw))) if err != nil { return JSAPIParams{}, err } defer resp.Body.Close() b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) var out unifiedResp if err := xml.Unmarshal(b, &out); err != nil { return JSAPIParams{}, err } if out.ReturnCode != "SUCCESS" || out.ResultCode != "SUCCESS" || out.PrepayID == "" { msg := out.ErrCodeDes if msg == "" { msg = out.ReturnMsg } if msg == "" { msg = "创建订单失败" } return JSAPIParams{}, fmt.Errorf("%s", msg) } return BuildJSAPI(cfg.AppID, out.PrepayID, cfg.APIKey), nil } func nonceStr() string { var b [8]byte _, _ = rand.Read(b[:]) return strings.ToUpper(hex.EncodeToString(b[:])) }