diff --git a/ymt_v3/client.go b/ymt_v3/client.go new file mode 100644 index 0000000..8764cf2 --- /dev/null +++ b/ymt_v3/client.go @@ -0,0 +1,240 @@ +package ymt_v3 + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// Env 环境 +type Env string + +const ( + EnvTest Env = "test" + EnvProduct Env = "product" +) + +// 环境地址 +const ( + testBaseURL = "https://gateway.dev.cdlsxd.cn" + productBaseURL = "https://market.api.86698.cn" +) + +// ClientConfig 客户端配置 +type ClientConfig struct { + AppID string // 应用ID + PrivateKey string // 应用私钥(PEM格式) + PublicKey string // 平台公钥(PEM格式) + Key string // 业务参数加密key + EncryptType EncryptType // 加密类型:aes 或 sm4 + Env Env // 环境:test 或 product + HTTPClient *http.Client // HTTP客户端,可选 +} + +// Client SDK客户端 +type Client struct { + config *ClientConfig + httpClient *http.Client + baseURL string +} + +// NewClient 创建新的SDK客户端 +func NewClient(config *ClientConfig) (*Client, error) { + if config.AppID == "" { + return nil, fmt.Errorf("AppID不能为空") + } + if config.PrivateKey == "" { + return nil, fmt.Errorf("PrivateKey不能为空") + } + if config.PublicKey == "" { + return nil, fmt.Errorf("PublicKey不能为空") + } + if config.Key == "" { + return nil, fmt.Errorf("Key不能为空") + } + if config.EncryptType == "" { + config.EncryptType = EncryptTypeAES + } + if config.Env == "" { + config.Env = EnvTest + } + + var baseURL string + switch config.Env { + case EnvTest: + baseURL = testBaseURL + case EnvProduct: + baseURL = productBaseURL + default: + return nil, fmt.Errorf("不支持的环境: %s", config.Env) + } + + httpClient := config.HTTPClient + if httpClient == nil { + httpClient = &http.Client{} + } + + return &Client{ + config: config, + httpClient: httpClient, + baseURL: baseURL, + }, nil +} + +// doRequest 发送请求并处理响应 +func (c *Client) doRequest(path string, bizParams interface{}, result interface{}) error { + // 1. 加密业务参数 + ciphertext, err := EncryptBizParams(bizParams, []byte(c.config.Key), c.config.EncryptType) + if err != nil { + return NewSDKError("加密业务参数失败", err) + } + + // 2. 生成时间戳 + timestamp := GenerateTimestamp() + + // 3. 构建签名字符串并签名 + signStr := BuildSignStr(c.config.AppID, timestamp, ciphertext) + sign, err := SignWithRSA(signStr, c.config.PrivateKey) + if err != nil { + return NewSDKError("签名失败", err) + } + + // 4. 构建请求体 + reqBody := map[string]string{ + "ciphertext": ciphertext, + } + reqBodyBytes, err := json.Marshal(reqBody) + if err != nil { + return NewSDKError("序列化请求体失败", err) + } + + // 5. 创建HTTP请求 + url := c.baseURL + path + req, err := http.NewRequest("POST", url, bytes.NewReader(reqBodyBytes)) + if err != nil { + return NewSDKError("创建HTTP请求失败", err) + } + + // 6. 设置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) + + // 7. 发送请求 + resp, err := c.httpClient.Do(req) + if err != nil { + return NewSDKError("发送HTTP请求失败", err) + } + defer resp.Body.Close() + + // 8. 读取响应 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return NewSDKError("读取响应失败", err) + } + + // 9. 解析公共响应 + var commonResp CommonResponse + if err := json.Unmarshal(respBody, &commonResp); err != nil { + return NewSDKError("解析响应JSON失败", err) + } + + // 10. 检查业务错误 + if commonResp.Code != 200 { + return NewAPIError(commonResp.Code, commonResp.Message, commonResp.Reason) + } + + // 11. 检查是否有加密数据 + if commonResp.Data == nil || commonResp.Data.Ciphertext == "" { + return NewSDKError("响应中没有加密数据", nil) + } + + // 12. 解密响应数据 + decrypted, err := DecryptBizParams(commonResp.Data.Ciphertext, []byte(c.config.Key), c.config.EncryptType) + if err != nil { + return NewSDKError("解密响应数据失败", err) + } + + // 13. 解析解密后的数据到结果 + if err := json.Unmarshal(decrypted, result); err != nil { + return NewSDKError("解析解密后的响应数据失败", err) + } + + return nil +} + +// ==================== API方法 ==================== + +// KeyOrder 获取券码 +// POST /openapi/v1/key/order +func (c *Client) KeyOrder(req *KeyOrderRequest) (*KeyOrderResponse, error) { + result := &KeyOrderResponse{} + if err := c.doRequest("/openapi/v1/key/order", req, result); err != nil { + return nil, err + } + return result, nil +} + +// KeyQuery 券码查询 +// POST /openapi/v1/key/query +func (c *Client) KeyQuery(req *KeyQueryRequest) (*KeyOrderResponse, error) { + result := &KeyOrderResponse{} + if err := c.doRequest("/openapi/v1/key/query", req, result); err != nil { + return nil, err + } + return result, nil +} + +// KeyDiscard 券码作废 +// POST /openapi/v1/key/discard +func (c *Client) KeyDiscard(req *KeyDiscardRequest) (*KeyDiscardResponse, error) { + result := &KeyDiscardResponse{} + if err := c.doRequest("/openapi/v1/key/discard", req, result); err != nil { + return nil, err + } + return result, nil +} + +// BatchOrder 批量发卡 +// POST /openapi/v1/key/batch_order +func (c *Client) BatchOrder(req *BatchOrderRequest) (*BatchOrderResponse, error) { + result := &BatchOrderResponse{} + if err := c.doRequest("/openapi/v1/key/batch_order", req, result); err != nil { + return nil, err + } + return result, nil +} + +// BatchQuery 批量查询 +// POST /openapi/v1/key/batch_query +func (c *Client) BatchQuery(req *BatchQueryRequest) (*BatchQueryResponse, error) { + result := &BatchQueryResponse{} + if err := c.doRequest("/openapi/v1/key/batch_query", req, result); err != nil { + return nil, err + } + return result, nil +} + +// VerifyNotify 验证回调通知 +func (c *Client) VerifyNotify(header *NotifyHeader, data interface{}) error { + return VerifyNotifySign(header, data, []byte(c.config.Key), c.config.PublicKey, c.config.EncryptType) +} + +// GetBaseURL 获取当前环境的基础URL +func (c *Client) GetBaseURL() string { + return c.baseURL +} + +// GetAppID 获取应用ID +func (c *Client) GetAppID() string { + return c.config.AppID +} + +// String 返回客户端配置的字符串表示(隐藏敏感信息) +func (c *Client) String() string { + return fmt.Sprintf("Client{AppID: %s, Env: %s, BaseURL: %s, EncryptType: %s}", + c.config.AppID, c.config.Env, c.baseURL, c.config.EncryptType) +} \ No newline at end of file