xy_sh-20260724180223/xy_sh/client.go

271 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
}