ymt_v3_2-20260721-151016/ymt_v3_2/client.go

188 lines
4.0 KiB
Go

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"`
}