添加文件: xy-shanghai/client.go
This commit is contained in:
parent
f65c54f488
commit
53228d8f2b
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue