186 lines
4.6 KiB
Go
186 lines
4.6 KiB
Go
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
|
||
} |