添加文件: xy_sh/client.go
This commit is contained in:
parent
1c356f99f3
commit
ff5cd40d7f
|
|
@ -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, ¬ifyReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ¬ifyReq, nil
|
||||
}
|
||||
|
||||
// NotifyResponseOK 生成回调通知成功响应
|
||||
// 行方处理成功后,返回HTTP 200且内容为"ok"
|
||||
func NotifyResponseOK() (int, string) {
|
||||
return http.StatusOK, "ok"
|
||||
}
|
||||
|
||||
// NotifyResponseFail 生成回调通知失败响应
|
||||
func NotifyResponseFail() (int, string) {
|
||||
return http.StatusInternalServerError, "fail"
|
||||
}
|
||||
Loading…
Reference in New Issue