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, "")
|
||
}
|