package ymt_v3_2 import ( "bytes" "context" "crypto/rsa" "encoding/json" "fmt" "io" "net/http" "time" ) type Client struct { httpClient *http.Client baseURL string appID string privateKey *rsa.PrivateKey publicKey *rsa.PublicKey aesKey []byte signType string } type Option func(*Client) func WithBaseURL(url string) Option { return func(c *Client) { c.baseURL = url } } func WithTimeout(d time.Duration) Option { return func(c *Client) { c.httpClient.Timeout = d } } func WithHTTPClient(client *http.Client) Option { return func(c *Client) { c.httpClient = client } } func WithAppID(appID string) Option { return func(c *Client) { c.appID = appID } } func WithPrivateKeyPEM(pemData string) Option { return func(c *Client) { key, err := parsePrivateKey(pemData) if err != nil { panic(fmt.Sprintf("failed to parse private key: %v", err)) } c.privateKey = key } } func WithPublicKeyPEM(pemData string) Option { return func(c *Client) { key, err := parsePublicKey(pemData) if err != nil { panic(fmt.Sprintf("failed to parse public key: %v", err)) } c.publicKey = key } } func WithAESKey(key string) Option { return func(c *Client) { c.aesKey = []byte(key) } } func WithSignType(signType string) Option { return func(c *Client) { c.signType = signType } } func NewClient(opts ...Option) *Client { c := &Client{ httpClient: &http.Client{Timeout: 30 * time.Second}, baseURL: "https://gateway.dev.cdlsxd.cn", signType: "RSA", } for _, opt := range opts { opt(c) } return c } func (c *Client) Do(ctx context.Context, method, path string, bizReq interface{}, bizResp interface{}) error { plaintext, err := marshalSortedNoZero(bizReq) if err != nil { return fmt.Errorf("marshal request: %w", err) } ciphertext, err := aesEncryptECB(plaintext, c.aesKey) if err != nil { return fmt.Errorf("encrypt: %w", err) } timestamp := time.Now().Format("2006-01-02 15:04:05") signStr := c.appID + timestamp + ciphertext sign, err := rsaSign([]byte(signStr), c.privateKey) if err != nil { return fmt.Errorf("sign: %w", err) } reqBody := cipherRequest{Ciphertext: ciphertext} bodyBytes, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("marshal request body: %w", err) } url := c.baseURL + path req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(bodyBytes)) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("Appid", c.appID) req.Header.Set("Timestamp", timestamp) req.Header.Set("Sign", sign) req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("http request: %w", err) } defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("read response: %w", err) } var commonResp commonResponse if err := json.Unmarshal(respBytes, &commonResp); err != nil { return fmt.Errorf("unmarshal response: %w", err) } if commonResp.Code != 200 { return &APIError{ Code: commonResp.Code, Message: commonResp.Message, Reason: commonResp.Reason, } } if len(commonResp.Data) == 0 { return nil } var cd cipherData if err := json.Unmarshal(commonResp.Data, &cd); err == nil && cd.Ciphertext != "" { decrypted, err := aesDecryptECB(cd.Ciphertext, c.aesKey) if err != nil { return fmt.Errorf("decrypt response: %w", err) } if err := json.Unmarshal(decrypted, bizResp); err != nil { return fmt.Errorf("unmarshal decrypted response: %w", err) } } else { if err := json.Unmarshal(commonResp.Data, bizResp); err != nil { return fmt.Errorf("unmarshal plain data: %w", err) } } return nil } type cipherRequest struct { Ciphertext string `json:"ciphertext"` } type commonResponse struct { Code int32 `json:"code"` Message string `json:"message"` Reason string `json:"reason,omitempty"` Data json.RawMessage `json:"data,omitempty"` } type cipherData struct { Ciphertext string `json:"ciphertext"` }