添加文件: xy_sh/client.go

This commit is contained in:
renzhiyuan 2026-07-24 18:02:27 +08:00
parent 1c356f99f3
commit ff5cd40d7f
1 changed files with 271 additions and 0 deletions

271
xy_sh/client.go Normal file
View File

@ -0,0 +1,271 @@
package xy_sh
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
// Client 供应商API客户端
type Client struct {
httpClient *http.Client
sm4Key []byte // SM4加密密钥16字节
sm3Salt string // SM3加盐哈希的盐值
baseURL string // 供应商接口基础地址
}
// ClientOption 客户端配置选项
type ClientOption func(*Client)
// WithHTTPClient 设置自定义HTTP客户端
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = httpClient
}
}
// NewClient 创建新的供应商API客户端
// sm4Key: SM4加密密钥必须是16字节
// sm3Salt: SM3加盐哈希的盐值
// baseURL: 供应商接口基础地址
func NewClient(sm4Key []byte, sm3Salt string, baseURL string, opts ...ClientOption) (*Client, error) {
if len(sm4Key) != 16 {
return nil, fmt.Errorf("%w: SM4密钥长度必须为16字节", ErrInvalidConfig)
}
if sm3Salt == "" {
return nil, fmt.Errorf("%w: SM3盐值不能为空", ErrInvalidConfig)
}
if baseURL == "" {
return nil, fmt.Errorf("%w: 接口地址不能为空", ErrInvalidConfig)
}
c := &Client{
httpClient: &http.Client{},
sm4Key: sm4Key,
sm3Salt: sm3Salt,
baseURL: baseURL,
}
for _, opt := range opts {
opt(c)
}
return c, nil
}
// ============================================================
// 内部方法:加密、签名、请求发送
// ============================================================
// encryptAndSign 对业务参数进行SM4加密并生成SM3签名
// 返回: encryptedData, timestamp, sign, error
func (c *Client) encryptAndSign(bizParams interface{}) (string, string, string, error) {
// 1. 将业务参数序列化为JSON
bizJSON, err := json.Marshal(bizParams)
if err != nil {
return "", "", "", fmt.Errorf("%w: 序列化业务参数失败: %v", ErrEncryptFailed, err)
}
// 2. SM4加密
encryptedData, err := SM4Encrypt(bizJSON, c.sm4Key)
if err != nil {
return "", "", "", fmt.Errorf("%w: %v", ErrEncryptFailed, err)
}
// 3. 生成毫秒级时间戳
timestamp := GenerateTimestampMillis()
// 4. 生成签名SM3(salt, timestamp + encryptedData)
signStr := timestamp + encryptedData
sign := SM3WithSaltString(signStr, c.sm3Salt)
return encryptedData, timestamp, sign, nil
}
// decryptResponseData 解密响应中的data字段
func (c *Client) decryptResponseData(encryptedData string, target interface{}) error {
// 1. SM4解密
plaintext, err := SM4Decrypt(encryptedData, c.sm4Key)
if err != nil {
return fmt.Errorf("%w: %v", ErrDecryptFailed, err)
}
// 2. JSON反序列化
if err := json.Unmarshal(plaintext, target); err != nil {
return fmt.Errorf("%w: 反序列化业务数据失败: %v", ErrResponseParseFailed, err)
}
return nil
}
// doRequest 发送加密请求并解密响应
func (c *Client) doRequest(ctx context.Context, url string, bizParams interface{}, responseData interface{}) (*EncryptedResponse, error) {
// 1. 加密业务参数并生成签名
encryptedData, timestamp, sign, err := c.encryptAndSign(bizParams)
if err != nil {
return nil, err
}
// 2. 构建请求体
reqBody := EncryptedRequest{
EncryptedData: encryptedData,
}
reqBodyJSON, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("%w: 序列化请求体失败: %v", ErrRequestFailed, err)
}
// 3. 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBodyJSON))
if err != nil {
return nil, fmt.Errorf("%w: 创建请求失败: %v", ErrRequestFailed, err)
}
// 4. 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("timestamp", timestamp)
req.Header.Set("sign", sign)
// 5. 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("%w: 发送请求失败: %v", ErrRequestFailed, err)
}
defer resp.Body.Close()
// 6. 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%w: 读取响应体失败: %v", ErrResponseParseFailed, err)
}
// 7. 解析响应
var encryptedResp EncryptedResponse
if err := json.Unmarshal(respBody, &encryptedResp); err != nil {
return nil, fmt.Errorf("%w: 解析响应JSON失败: %v", ErrResponseParseFailed, err)
}
// 8. 如果业务失败,直接返回
if encryptedResp.Code != 0 {
return &encryptedResp, nil
}
// 9. 如果data不为空解密data
if encryptedResp.Data != nil && *encryptedResp.Data != "" && responseData != nil {
if err := c.decryptResponseData(*encryptedResp.Data, responseData); err != nil {
return nil, err
}
}
return &encryptedResp, nil
}
// ============================================================
// 接口1卡券/直充权益下单接口
// ============================================================
// PlaceOrder 卡券/直充权益下单
// 适用于卡密,直充商品下单
func (c *Client) PlaceOrder(ctx context.Context, req *PlaceOrderRequest) (*EncryptedResponse, *PlaceOrderResponseData, error) {
url := c.baseURL + "/placeOrder"
var data PlaceOrderResponseData
resp, err := c.doRequest(ctx, url, req, &data)
if err != nil {
return nil, nil, err
}
if resp.Code != 0 {
return resp, nil, nil
}
return resp, &data, nil
}
// ============================================================
// 接口2卡券/直充/微信立减金订单查询接口
// ============================================================
// QueryOrder 查询订单状态
// 通过供应商订单号查询订单状态
func (c *Client) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*EncryptedResponse, *QueryOrderResponseData, error) {
url := c.baseURL + "/queryOrder"
var data QueryOrderResponseData
resp, err := c.doRequest(ctx, url, req, &data)
if err != nil {
return nil, nil, err
}
if resp.Code != 0 {
return resp, nil, nil
}
return resp, &data, nil
}
// ============================================================
// 接口3微信立减金订单充值接口
// ============================================================
// WechatRecharge 微信立减金订单充值
func (c *Client) WechatRecharge(ctx context.Context, req *WechatRechargeRequest) (*EncryptedResponse, *WechatRechargeResponseData, error) {
url := c.baseURL + "/wechatRecharge"
var data WechatRechargeResponseData
resp, err := c.doRequest(ctx, url, req, &data)
if err != nil {
return nil, nil, err
}
if resp.Code != 0 {
return resp, nil, nil
}
return resp, &data, nil
}
// ============================================================
// 接口4卡券/直充/微信立减金充值结果通知接口(服务端处理)
// ============================================================
// ParseNotifyRequest 解析回调通知请求
// 供应商主动推送充值结果通知时,行方使用此方法解析请求
// 返回: 解密后的通知请求参数, error
func (c *Client) ParseNotifyRequest(r *http.Request) (*NotifyRequest, error) {
// 1. 读取请求体
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %v", err)
}
defer r.Body.Close()
// 2. 解析请求体
var encryptedReq EncryptedRequest
if err := json.Unmarshal(body, &encryptedReq); err != nil {
return nil, fmt.Errorf("解析请求体JSON失败: %v", err)
}
// 3. 获取请求头中的timestamp和sign
timestamp := r.Header.Get("timestamp")
sign := r.Header.Get("sign")
// 4. 验证签名
expectedSign := SM3WithSaltString(timestamp+encryptedReq.EncryptedData, c.sm3Salt)
if sign != expectedSign {
return nil, fmt.Errorf("签名验证失败")
}
// 5. 解密业务数据
var notifyReq NotifyRequest
if err := c.decryptResponseData(encryptedReq.EncryptedData, &notifyReq); err != nil {
return nil, err
}
return &notifyReq, nil
}
// NotifyResponseOK 生成回调通知成功响应
// 行方处理成功后返回HTTP 200且内容为"ok"
func NotifyResponseOK() (int, string) {
return http.StatusOK, "ok"
}
// NotifyResponseFail 生成回调通知失败响应
func NotifyResponseFail() (int, string) {
return http.StatusInternalServerError, "fail"
}