添加文件: 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