xy_sh-20260724174214/xy_sh/client.go

244 lines
6.4 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 供应商接口客户端
type Client struct {
httpClient *http.Client
sm4Key []byte // SM4加密密钥16字节
sm3Salt []byte // SM3签名盐值
orderURL string // 下单接口地址
queryURL string // 查询接口地址
rechargeURL string // 微信立减金充值接口地址
}
// NewClient 创建新的客户端
// - sm4Key: SM4加密密钥必须为16字节
// - sm3Salt: SM3签名盐值
// - orderURL: 下单接口地址
// - queryURL: 查询接口地址
// - rechargeURL: 微信立减金充值接口地址
func NewClient(sm4Key []byte, sm3Salt []byte, orderURL, queryURL, rechargeURL string) (*Client, error) {
if len(sm4Key) != 16 {
return nil, fmt.Errorf("SM4密钥长度必须为16字节")
}
if len(sm3Salt) == 0 {
return nil, fmt.Errorf("SM3盐值不能为空")
}
return &Client{
httpClient: &http.Client{},
sm4Key: sm4Key,
sm3Salt: sm3Salt,
orderURL: orderURL,
queryURL: queryURL,
rechargeURL: rechargeURL,
}, nil
}
// SetHTTPClient 设置自定义HTTP客户端
func (c *Client) SetHTTPClient(httpClient *http.Client) {
c.httpClient = httpClient
}
// doRequest 执行加密请求并解密响应
func (c *Client) doRequest(ctx context.Context, url string, bizReq interface{}) (*RawResponse, error) {
// 1. 序列化业务参数为JSON
bizBody, err := json.Marshal(bizReq)
if err != nil {
return nil, fmt.Errorf("序列化业务参数失败: %v", err)
}
// 2. SM4加密业务参数
encryptedData, err := SM4Encrypt(bizBody, c.sm4Key)
if err != nil {
return nil, fmt.Errorf("SM4加密失败: %v", err)
}
// 3. 生成时间戳和签名
timestamp := GenerateTimestampMillis()
sign := GenerateSign(timestamp, encryptedData, c.sm3Salt)
// 4. 构建加密请求体
reqBody := EncryptedRequest{EncryptedData: encryptedData}
reqBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %v", err)
}
// 5. 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBytes))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("timestamp", timestamp)
req.Header.Set("sign", sign)
// 6. 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 7. 读取响应体
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %v", err)
}
// 8. 解析原始响应
var rawResp RawResponse
if err := json.Unmarshal(respBytes, &rawResp); err != nil {
return nil, fmt.Errorf("解析响应JSON失败: %v", err)
}
return &rawResp, nil
}
// decryptData 解密响应中的data字段
func (c *Client) decryptData(encryptedData string, target interface{}) error {
if encryptedData == "" {
return nil
}
plaintext, err := SM4Decrypt(encryptedData, c.sm4Key)
if err != nil {
return fmt.Errorf("SM4解密失败: %v", err)
}
if err := json.Unmarshal(plaintext, target); err != nil {
return fmt.Errorf("解析解密后数据失败: %v", err)
}
return nil
}
// Order 卡券/直充权益下单
// 适用于卡密、直充商品下单
func (c *Client) Order(ctx context.Context, req *OrderRequest) (*OrderResponse, error) {
rawResp, err := c.doRequest(ctx, c.orderURL, req)
if err != nil {
return nil, err
}
resp := &OrderResponse{
Code: rawResp.Code,
Msg: rawResp.Msg,
}
if rawResp.Data != "" {
var data OrderData
if err := c.decryptData(rawResp.Data, &data); err != nil {
return nil, err
}
resp.Data = &data
}
return resp, nil
}
// QueryOrder 卡券/直充/微信立减金订单查询
// 通过供应商订单号查询订单状态
func (c *Client) QueryOrder(ctx context.Context, req *QueryRequest) (*QueryResponse, error) {
rawResp, err := c.doRequest(ctx, c.queryURL, req)
if err != nil {
return nil, err
}
resp := &QueryResponse{
Code: rawResp.Code,
Msg: rawResp.Msg,
}
if rawResp.Data != "" {
var data QueryData
if err := c.decryptData(rawResp.Data, &data); err != nil {
return nil, err
}
resp.Data = &data
}
return resp, nil
}
// WechatRecharge 微信立减金订单充值
func (c *Client) WechatRecharge(ctx context.Context, req *WechatRechargeRequest) (*WechatRechargeResponse, error) {
rawResp, err := c.doRequest(ctx, c.rechargeURL, req)
if err != nil {
return nil, err
}
resp := &WechatRechargeResponse{
Code: rawResp.Code,
Msg: rawResp.Msg,
}
if rawResp.Data != "" {
var data WechatRechargeData
if err := c.decryptData(rawResp.Data, &data); err != nil {
return nil, err
}
resp.Data = &data
}
return resp, nil
}
// SendCallback 发送回调通知(供应商调用行方回调接口)
// 返回的 CallbackResponse 中 StatusCode 为 HTTP 状态码Body 为响应体内容
func (c *Client) SendCallback(ctx context.Context, callbackURL string, req *CallbackRequest) (*CallbackResponse, error) {
// 1. 序列化业务参数
bizBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("序列化回调参数失败: %v", err)
}
// 2. SM4加密
encryptedData, err := SM4Encrypt(bizBody, c.sm4Key)
if err != nil {
return nil, fmt.Errorf("SM4加密失败: %v", err)
}
// 3. 生成时间戳和签名
timestamp := GenerateTimestampMillis()
sign := GenerateSign(timestamp, encryptedData, c.sm3Salt)
// 4. 构建请求体
reqBody := EncryptedRequest{EncryptedData: encryptedData}
reqBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %v", err)
}
// 5. 创建HTTP请求
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, callbackURL, bytes.NewReader(reqBytes))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("timestamp", timestamp)
httpReq.Header.Set("sign", sign)
// 6. 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("发送回调请求失败: %v", err)
}
defer resp.Body.Close()
// 7. 读取响应体
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取回调响应失败: %v", err)
}
return &CallbackResponse{
StatusCode: resp.StatusCode,
Body: string(respBytes),
}, nil
}