From 769b9b4ddc86235478574157a00bdc2cf94ba818 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 23 Jul 2026 17:07:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20ymt=5Fv?= =?UTF-8?q?3/client.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ymt_v3/client.go | 249 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 ymt_v3/client.go diff --git a/ymt_v3/client.go b/ymt_v3/client.go new file mode 100644 index 0000000..a1c176d --- /dev/null +++ b/ymt_v3/client.go @@ -0,0 +1,249 @@ +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) + } +} \ No newline at end of file