From 649e43b41b196c07e91f25f50217edb0347bf2f2 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Tue, 21 Jul 2026 10:13:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20ymt=5Fv?= =?UTF-8?q?3=5Fgenerate/client.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ymt_v3_generate/client.go | 181 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 ymt_v3_generate/client.go diff --git a/ymt_v3_generate/client.go b/ymt_v3_generate/client.go new file mode 100644 index 0000000..38526d0 --- /dev/null +++ b/ymt_v3_generate/client.go @@ -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 +} \ No newline at end of file