package xyshanghai import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // Client 是 SDK 的客户端,用于调用供应商的 API 接口。 type Client struct { baseURL string httpClient *http.Client sm4Key []byte sm3Salt []byte } // Option 定义客户端配置选项。 type Option func(*Client) // NewClient 创建一个新的 Client 实例。 // 必须通过 WithSM4Key 和 WithSM3Salt 设置密钥和盐值。 func NewClient(opts ...Option) *Client { c := &Client{ httpClient: &http.Client{Timeout: 30 * time.Second}, } for _, opt := range opts { opt(c) } return c } // WithBaseURL 设置基础 URL。 func WithBaseURL(baseURL string) Option { return func(c *Client) { c.baseURL = baseURL } } // WithTimeout 设置 HTTP 请求超时时间。 func WithTimeout(timeout time.Duration) Option { return func(c *Client) { c.httpClient.Timeout = timeout } } // WithHTTPClient 设置自定义 HTTP 客户端。 func WithHTTPClient(httpClient *http.Client) Option { return func(c *Client) { c.httpClient = httpClient } } // WithSM4Key 设置 SM4 加密密钥(16 字节)。 func WithSM4Key(key []byte) Option { return func(c *Client) { c.sm4Key = key } } // WithSM3Salt 设置 SM3 签名盐值。 func WithSM3Salt(salt []byte) Option { return func(c *Client) { c.sm3Salt = salt } } // doRequest 发送 POST 请求,自动加密业务数据并签名。 func (c *Client) doRequest(ctx context.Context, path string, bizData interface{}) (*CommonResponse, error) { if c.baseURL == "" { return nil, ErrBaseURLNotSet } if len(c.sm4Key) == 0 { return nil, ErrSM4KeyNotSet } if len(c.sm3Salt) == 0 { return nil, ErrSM3SaltNotSet } // 序列化业务数据 bizJSON, err := json.Marshal(bizData) if err != nil { return nil, fmt.Errorf("marshal biz data: %w", err) } // SM4 加密 encryptedData, err := SM4Encrypt(c.sm4Key, bizJSON) if err != nil { return nil, fmt.Errorf("sm4 encrypt: %w", err) } // 生成时间戳(毫秒) timestamp := fmt.Sprintf("%d", time.Now().UnixMilli()) // 计算签名 sign := SM3WithSalt(c.sm3Salt, timestamp+string(encryptedData)) // 构建请求体 reqBody := &EncryptedRequest{ EncryptedData: string(encryptedData), } reqJSON, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("marshal request body: %w", err) } // 创建 HTTP 请求 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(reqJSON)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("timestamp", timestamp) req.Header.Set("sign", sign) // 发送请求 resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("http do: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response body: %w", err) } // 解析公共响应 var commonResp CommonResponse if err := json.Unmarshal(body, &commonResp); err != nil { return nil, fmt.Errorf("unmarshal common response: %w", err) } // 如果 data 不为空,解密 data 字段 if commonResp.Data != "" { decrypted, err := SM4Decrypt(c.sm4Key, []byte(commonResp.Data)) if err != nil { return nil, fmt.Errorf("sm4 decrypt response data: %w", err) } commonResp.Data = string(decrypted) } return &commonResp, nil } // CreateOrder 卡券/直充权益下单接口。 func (c *Client) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CommonResponse, error) { return c.doRequest(ctx, "/createOrder", req) } // QueryOrder 卡券/直充/微信立减金订单查询接口。 func (c *Client) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*CommonResponse, error) { return c.doRequest(ctx, "/queryOrder", req) } // RechargeOrder 微信立减金订单充值接口。 func (c *Client) RechargeOrder(ctx context.Context, req *RechargeOrderRequest) (*CommonResponse, error) { return c.doRequest(ctx, "/rechargeOrder", req) } // ParseCallbackRequest 解析回调请求,验证签名并解密业务数据。 // 返回解密后的业务数据(JSON 字符串)和错误。 func (c *Client) ParseCallbackRequest(timestamp, sign string, encryptedData string) (string, error) { if len(c.sm3Salt) == 0 { return "", ErrSM3SaltNotSet } if len(c.sm4Key) == 0 { return "", ErrSM4KeyNotSet } // 验证签名 expectedSign := SM3WithSalt(c.sm3Salt, timestamp+encryptedData) if sign != expectedSign { return "", ErrInvalidSign } // 解密 decrypted, err := SM4Decrypt(c.sm4Key, []byte(encryptedData)) if err != nil { return "", fmt.Errorf("sm4 decrypt callback data: %w", err) } return string(decrypted), nil }