121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
|
|
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, "")
|
|||
|
|
}
|