ymt_v3_generate-20260721-10.../ymt_v3_generate/client.go

181 lines
4.2 KiB
Go
Raw Permalink 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 ymt_v3_generate
import (
"bytes"
"context"
"crypto/rsa"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client 蓝色兄弟营销开放 API 客户端
type Client struct {
appID string
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
key []byte // AES 加密密钥
baseURL string
httpClient *http.Client
timeout time.Duration
}
// Option 客户端配置选项
type Option func(*Client)
// NewClient 创建客户端
func NewClient(opts ...Option) *Client {
c := &Client{
baseURL: "https://gateway.dev.cdlsxd.cn", // 默认测试环境
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
for _, opt := range opts {
opt(c)
}
return c
}
// WithAppID 设置应用 ID
func WithAppID(appID string) Option {
return func(c *Client) {
c.appID = appID
}
}
// WithPrivateKey 设置私钥PEM 字符串)
func WithPrivateKey(privateKeyPEM string) Option {
return func(c *Client) {
key, err := ParsePrivateKey(privateKeyPEM)
if err != nil {
panic(fmt.Sprintf("invalid private key: %v", err))
}
c.privateKey = key
}
}
// WithPublicKey 设置公钥PEM 字符串)
func WithPublicKey(publicKeyPEM string) Option {
return func(c *Client) {
key, err := ParsePublicKey(publicKeyPEM)
if err != nil {
panic(fmt.Sprintf("invalid public key: %v", err))
}
c.publicKey = key
}
}
// WithKey 设置 AES 加密密钥base64 字符串)
func WithKey(keyBase64 string) Option {
return func(c *Client) {
key, err := base64.StdEncoding.DecodeString(keyBase64)
if err != nil {
panic(fmt.Sprintf("invalid AES key: %v", err))
}
c.key = key
}
}
// WithBaseURL 设置基础 URL
func WithBaseURL(baseURL string) Option {
return func(c *Client) {
c.baseURL = baseURL
}
}
// WithTimeout 设置超时时间
func WithTimeout(timeout time.Duration) Option {
return func(c *Client) {
c.timeout = timeout
c.httpClient.Timeout = timeout
}
}
// WithHTTPClient 设置自定义 HTTP 客户端
func WithHTTPClient(httpClient *http.Client) Option {
return func(c *Client) {
c.httpClient = httpClient
}
}
// Do 发送 API 请求
func (c *Client) Do(ctx context.Context, method, path string, requestBody interface{}, responseBody interface{}) error {
// 1. 将请求体转为排序后的 JSON 字符串plaintext
plaintext, err := RemoveZeroValuesAndSort(requestBody)
if err != nil {
return fmt.Errorf("failed to serialize request body: %w", err)
}
// 2. 加密得到 ciphertext
ciphertext, err := EncryptPlaintext(plaintext, c.key)
if err != nil {
return fmt.Errorf("failed to encrypt request: %w", err)
}
// 3. 生成时间戳
timestamp := time.Now().Format("2006-01-02 15:04:05")
// 4. 生成签名
sign, err := Sign(c.appID, timestamp, ciphertext, c.privateKey)
if err != nil {
return fmt.Errorf("failed to sign request: %w", err)
}
// 5. 构造 HTTP 请求
reqBody := map[string]string{"ciphertext": ciphertext}
reqBytes, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(reqBytes))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Appid", c.appID)
req.Header.Set("Timestamp", timestamp)
req.Header.Set("Sign", sign)
// 6. 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
// 7. 解析公共响应
var apiResp ApiResponse
if err := json.Unmarshal(respBytes, &apiResp); err != nil {
return fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Code != 200 {
return &APIError{
Code: apiResp.Code,
Message: apiResp.Message,
Reason: apiResp.Reason,
}
}
// 8. 解密 data.ciphertext
decrypted, err := DecryptCiphertext(apiResp.Data.Ciphertext, c.key)
if err != nil {
return fmt.Errorf("failed to decrypt response: %w", err)
}
// 9. 反序列化到业务响应
if err := json.Unmarshal([]byte(decrypted), responseBody); err != nil {
return fmt.Errorf("failed to unmarshal business response: %w", err)
}
return nil
}