diff --git a/xy_sh/client.go b/xy_sh/client.go new file mode 100644 index 0000000..ea3f38a --- /dev/null +++ b/xy_sh/client.go @@ -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" +} \ No newline at end of file