Files
soul-yongping/soul-api/internal/wechat/transferv3/client.go

121 lines
3.1 KiB
Go
Raw Normal View History

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