package transferv3 import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "io" "net/http" "net/url" "os" "strconv" "time" ) const wechatAPIBase = "https://api.mch.weixin.qq.com" func nonce() string { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" b := make([]byte, 32) _, _ = rand.Read(b) for i := range b { b[i] = chars[int(b[i])%len(chars)] } return string(b) } // Client 文档 V3 商家转账到零钱(签名 + HTTP) type Client struct { MchID string AppID string SerialNo string PrivateKey *rsa.PrivateKey BaseURL string } // NewClient 使用已有私钥创建 Client func NewClient(mchID, appID, serialNo string, privateKey *rsa.PrivateKey) *Client { base := wechatAPIBase return &Client{ MchID: mchID, AppID: appID, SerialNo: serialNo, PrivateKey: privateKey, BaseURL: base, } } // LoadPrivateKeyFromPath 从 PEM 文件路径加载商户私钥 func LoadPrivateKeyFromPath(path string) (*rsa.PrivateKey, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } return LoadPrivateKeyFromPEM(data) } // LoadPrivateKeyFromPEM 从 PEM 内容解析商户私钥(支持 PKCS#1 或 PKCS#8) func LoadPrivateKeyFromPEM(pemContent []byte) (*rsa.PrivateKey, error) { block, _ := pem.Decode(pemContent) if block == nil { return nil, fmt.Errorf("no PEM block found") } key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err == nil { return key, nil } k, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } rsaKey, ok := k.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("not RSA private key") } return rsaKey, nil } // do 带签名的 HTTP 请求 func (c *Client) do(method, path, body string) ([]byte, int, error) { timestamp := strconv.FormatInt(time.Now().Unix(), 10) nonceStr := nonce() signMsg := BuildSignMessage(method, path, timestamp, nonceStr, body) sig, err := Sign(signMsg, c.PrivateKey) if err != nil { return nil, 0, err } auth := BuildAuthorization(c.MchID, nonceStr, sig, timestamp, c.SerialNo) fullURL := c.BaseURL + path req, err := http.NewRequest(method, fullURL, bytes.NewBufferString(body)) if err != nil { return nil, 0, err } req.Header.Set("Authorization", auth) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, 0, err } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) return data, resp.StatusCode, nil } // PostBatches 发起转账(文档:POST /v3/transfer/batches) func (c *Client) PostBatches(body []byte) ([]byte, int, error) { return c.do("POST", "/v3/transfer/batches", string(body)) } // GetTransferDetail 按商户批次单号、商户明细单号查询(文档:GET .../batch-id/{}/details/detail-id/{}) func (c *Client) GetTransferDetail(outBatchNo, outDetailNo string) ([]byte, int, error) { path := "/v3/transfer/batches/batch-id/" + url.PathEscape(outBatchNo) + "/details/detail-id/" + url.PathEscape(outDetailNo) return c.do("GET", path, "") }