添加文件: ymt_v3_generate/client.go
This commit is contained in:
parent
3c626ba75f
commit
649e43b41b
|
|
@ -0,0 +1,181 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in New Issue