小程序可在 Go 上完成微信手机号登录、测评、预约和下单,不再反代 Java。 Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
4.2 KiB
Go
159 lines
4.2 KiB
Go
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[:]))
|
|
}
|