添加文件: xy-shanghai/client.go

This commit is contained in:
renzhiyuan 2026-07-21 17:23:46 +08:00
parent f65c54f488
commit 53228d8f2b
1 changed files with 186 additions and 0 deletions

186
xy-shanghai/client.go Normal file
View File

@ -0,0 +1,186 @@
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
}