xy-shanghai-20260721-171915/xy-shanghai/client.go

186 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}