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) } }