Files
soul-yongping/soul-api/internal/wechat/transferv3/client.go
2026-02-25 11:52:11 +08:00

121 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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