ymt_v3-20260723170706/ymt_v3/client.go

249 lines
6.7 KiB
Go
Raw Permalink 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 ymt_v3
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
)
// Config 客户端配置
type Config struct {
AppID string // 应用ID
PrivateKey string // 应用私钥PEM格式用于请求签名
PublicKey string // 平台公钥PEM格式用于响应验签
Key string // 业务参数加密密钥
EncryptType string // 加密类型aes 或 sm4
BaseURL string // 接口地址
HTTPClient *http.Client
}
// Client SDK客户端
type Client struct {
config *Config
client *http.Client
}
// NewClient 创建新的SDK客户端
func NewClient(config *Config) *Client {
if config.HTTPClient == nil {
config.HTTPClient = http.DefaultClient
}
return &Client{
config: config,
client: config.HTTPClient,
}
}
// doRequest 发送请求并处理响应
func (c *Client) doRequest(ctx context.Context, path string, bizParams interface{}, result interface{}) error {
// 1. 加密业务参数
ciphertext, err := EncryptBizParams(bizParams, []byte(c.config.Key), c.config.EncryptType)
if err != nil {
return fmt.Errorf("加密业务参数失败: %v", err)
}
// 2. 生成时间戳
timestamp := GenerateTimestamp()
// 3. 拼接签名字符串app_id + timestamp + ciphertext
signStr := c.config.AppID + timestamp + ciphertext
// 4. 使用应用私钥签名
sign, err := SignWithRSA(signStr, c.config.PrivateKey)
if err != nil {
return fmt.Errorf("签名失败: %v", err)
}
// 5. 构建请求体
reqBody := EncryptedRequest{
Ciphertext: ciphertext,
}
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("序列化请求体失败: %v", err)
}
// 6. 创建HTTP请求
url := strings.TrimRight(c.config.BaseURL, "/") + path
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("创建请求失败: %v", err)
}
// 7. 设置Header
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Appid", c.config.AppID)
req.Header.Set("Timestamp", timestamp)
req.Header.Set("Sign", sign)
// 8. 发送请求
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %v", err)
}
// 9. 解析响应
var encryptedResp EncryptedResponse
if err := json.Unmarshal(respBody, &encryptedResp); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
// 10. 检查错误码
if encryptedResp.Code != 200 {
return &APIError{
Code: encryptedResp.Code,
Message: encryptedResp.Message,
Reason: encryptedResp.Reason,
}
}
// 11. 解密响应数据
if encryptedResp.Data == nil || encryptedResp.Data.Ciphertext == "" {
return fmt.Errorf("响应数据为空")
}
plaintext, err := DecryptBizParams(encryptedResp.Data.Ciphertext, []byte(c.config.Key), c.config.EncryptType)
if err != nil {
return fmt.Errorf("解密响应数据失败: %v", err)
}
// 12. 解析解密后的数据到结果结构体
if err := json.Unmarshal(plaintext, result); err != nil {
return fmt.Errorf("解析解密数据失败: %v", err)
}
return nil
}
// ============================================================
// API 方法
// ============================================================
// Order 获取券码
func (c *Client) Order(ctx context.Context, req *OrderRequest) (*OrderResponse, error) {
result := &OrderResponse{}
if err := c.doRequest(ctx, "/openapi/v1/key/order", req, result); err != nil {
return nil, err
}
return result, nil
}
// Query 券码查询
func (c *Client) Query(ctx context.Context, req *QueryRequest) (*OrderResponse, error) {
result := &OrderResponse{}
if err := c.doRequest(ctx, "/openapi/v1/key/query", req, result); err != nil {
return nil, err
}
return result, nil
}
// Discard 券码作废
func (c *Client) Discard(ctx context.Context, req *DiscardRequest) (*DiscardResponse, error) {
result := &DiscardResponse{}
if err := c.doRequest(ctx, "/openapi/v1/key/discard", req, result); err != nil {
return nil, err
}
return result, nil
}
// BatchOrder 批量发卡
func (c *Client) BatchOrder(ctx context.Context, req *BatchOrderRequest) (*BatchOrderResponse, error) {
result := &BatchOrderResponse{}
if err := c.doRequest(ctx, "/openapi/v1/key/batch_order", req, result); err != nil {
return nil, err
}
return result, nil
}
// BatchQuery 批量查询
func (c *Client) BatchQuery(ctx context.Context, req *BatchQueryRequest) (*BatchQueryResponse, error) {
result := &BatchQueryResponse{}
if err := c.doRequest(ctx, "/openapi/v1/key/batch_query", req, result); err != nil {
return nil, err
}
return result, nil
}
// ============================================================
// 回调验签
// ============================================================
// VerifyCallback 验证回调通知的签名
// 参数:
// - appID: 应用ID
// - timestamp: 回调请求中的时间戳
// - sign: 回调请求中的签名
// - data: 回调请求中的业务数据明文JSON字符串
// - publicKeyPEM: 平台公钥PEM格式
// - key: 业务参数加密密钥
// - encryptType: 加密类型aes 或 sm4
func VerifyCallback(appID, timestamp, sign, data, publicKeyPEM string, key []byte, encryptType string) error {
// 1. 使用key将data加密得到ciphertext
ciphertext, err := EncryptBizParamsFromJSON(data, key, encryptType)
if err != nil {
return fmt.Errorf("加密回调数据失败: %v", err)
}
// 2. 拼接签名字符串app_id + timestamp + ciphertext
signStr := appID + timestamp + ciphertext
// 3. 使用平台公钥验签
return VerifyWithRSA(signStr, sign, publicKeyPEM)
}
// EncryptBizParamsFromJSON 将JSON字符串按规则加密
func EncryptBizParamsFromJSON(jsonStr string, key []byte, encryptType string) (string, error) {
// 解析JSON
var data map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return "", fmt.Errorf("解析JSON失败: %v", err)
}
// 去掉零值
cleaned := make(map[string]interface{})
for k, v := range data {
if !isZeroValue(v) {
cleaned[k] = v
}
}
// 按key排序
keys := make([]string, 0, len(cleaned))
for k := range cleaned {
keys = append(keys, k)
}
sort.Strings(keys)
// 构建有序map
orderedMap := make(map[string]interface{})
for _, k := range keys {
orderedMap[k] = cleaned[k]
}
// 转JSON
plaintext, err := json.Marshal(orderedMap)
if err != nil {
return "", fmt.Errorf("JSON序列化失败: %v", err)
}
// 加密
switch encryptType {
case "aes":
return AESECBEncrypt(plaintext, key)
case "sm4":
return SM4CBCEncrypt(plaintext, key)
default:
return "", fmt.Errorf("不支持的加密类型: %s", encryptType)
}
}