181 lines
4.2 KiB
Go
181 lines
4.2 KiB
Go
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
|
||
} |