package ymt_v3_api import ( "bytes" "context" "crypto/rsa" "encoding/json" "fmt" "io" "net/http" "time" ) // Client 是营销开放平台 SDK 的客户端,封装了认证、加密、签名等逻辑。 type Client struct { httpClient *http.Client baseURL string appID string privateKey *rsa.PrivateKey publicKey *rsa.PublicKey encryptKey []byte encryptType string // "AES" 或 "SM4" } // Option 是客户端配置选项函数。 type Option func(*Client) // WithBaseURL 设置 API 基础地址。 func WithBaseURL(url string) Option { return func(c *Client) { c.baseURL = url } } // WithTimeout 设置 HTTP 请求超时时间。 func WithTimeout(d time.Duration) Option { return func(c *Client) { c.httpClient.Timeout = d } } // WithHTTPClient 设置自定义的 HTTP 客户端。 func WithHTTPClient(client *http.Client) Option { return func(c *Client) { c.httpClient = client } } // WithEncryptType 设置业务参数加密方式,支持 "AES" (默认) 或 "SM4"。 func WithEncryptType(t string) Option { return func(c *Client) { c.encryptType = t } } // NewClient 创建一个新的客户端实例。 // appID: 应用 ID // privateKey: 应用私钥(PEM 格式字符串) // publicKey: 平台公钥(PEM 格式字符串) // encryptKey: 业务参数加解密密钥(字符串,AES 或 SM4 密钥) func NewClient(appID, privateKey, publicKey, encryptKey string, opts ...Option) (*Client, error) { priv, err := parsePrivateKey(privateKey) if err != nil { return nil, fmt.Errorf("parse private key: %w", err) } pub, err := parsePublicKey(publicKey) if err != nil { return nil, fmt.Errorf("parse public key: %w", err) } c := &Client{ httpClient: &http.Client{ Timeout: 30 * time.Second, }, baseURL: "https://market.api.86698.cn", // 默认正式环境 appID: appID, privateKey: priv, publicKey: pub, encryptKey: []byte(encryptKey), encryptType: "AES", } for _, opt := range opts { opt(c) } return c, nil } // doRequest 执行一次完整的 API 调用:加密业务参数、签名、发送请求、解密响应。 func (c *Client) doRequest(ctx context.Context, path string, bizReq interface{}, bizResp interface{}) error { // 1. 业务参数序列化为排序 JSON 并加密 ciphertext, err := c.encryptPayload(bizReq) if err != nil { return fmt.Errorf("encrypt payload: %w", err) } // 2. 生成时间戳 timestamp := time.Now().Format("2006-01-02 15:04:05") // 3. 签名 sign, err := c.sign(c.appID, timestamp, ciphertext) if err != nil { return fmt.Errorf("sign: %w", err) } // 4. 构建请求体 reqBody := cipherRequest{Ciphertext: ciphertext} bodyBytes, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("marshal request body: %w", err) } // 5. 创建 HTTP 请求 url := c.baseURL + path req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyBytes)) if err != nil { return fmt.Errorf("new request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Appid", c.appID) req.Header.Set("Timestamp", timestamp) req.Header.Set("Sign", sign) // 6. 发送请求 resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("http do: %w", err) } defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("read response body: %w", err) } // 7. 解析公共响应 var apiResp apiResponse if err := json.Unmarshal(respBytes, &apiResp); err != nil { return fmt.Errorf("unmarshal api response: %w", err) } if apiResp.Code != 200 { return &APIError{ Code: apiResp.Code, Message: apiResp.Message, Reason: apiResp.Reason, } } // 8. 解密业务响应 if apiResp.Data == nil || apiResp.Data.Ciphertext == "" { // 某些接口可能直接返回 data 对象(如查询接口文档示例中 data 直接包含字段,但实际是 ciphertext 为空?) // 根据文档,成功时 data.ciphertext 存在,但查询接口示例中 data 直接是业务字段,没有 ciphertext 包裹。 // 仔细看文档:获取券码响应成功时 data 里有 ciphertext;券码查询响应示例中 data 直接是业务字段,没有 ciphertext。 // 但文档说“响应中的 data.ciphertext 使用与请求相同的加密方式和 key 进行解密”,可能查询接口也加密了,但示例展示了解密后的。 // 为了兼容,如果 data.ciphertext 为空,尝试直接将 data 整体反序列化为业务响应。 // 这里我们统一处理:如果 ciphertext 非空则解密,否则直接解析 data 字段。 var resp struct { Data json.RawMessage `json:"data"` } if err := json.Unmarshal(respBytes, &resp); err != nil { return fmt.Errorf("unmarshal data field: %w", err) } return json.Unmarshal(resp.Data, bizResp) } if err := c.decryptPayload(apiResp.Data.Ciphertext, bizResp); err != nil { return fmt.Errorf("decrypt payload: %w", err) } return nil } // encryptPayload 将业务请求对象加密为 ciphertext 字符串。 func (c *Client) encryptPayload(payload interface{}) (string, error) { plaintext, err := toSortedJSON(payload) if err != nil { return "", err } switch c.encryptType { case "SM4": return sm4CbcEncrypt(plaintext, c.encryptKey) default: return aesEcbEncrypt(plaintext, c.encryptKey) } } // decryptPayload 将 ciphertext 解密并反序列化到目标对象。 func (c *Client) decryptPayload(ciphertext string, target interface{}) error { var plaintext []byte var err error switch c.encryptType { case "SM4": plaintext, err = sm4CbcDecrypt(ciphertext, c.encryptKey) default: plaintext, err = aesEcbDecrypt(ciphertext, c.encryptKey) } if err != nil { return err } return json.Unmarshal(plaintext, target) } // sign 生成请求签名。 func (c *Client) sign(appID, timestamp, ciphertext string) (string, error) { data := appID + timestamp + ciphertext return rsaSign([]byte(data), c.privateKey) } // verifySign 验证回调签名。 func (c *Client) verifySign(appID, timestamp, ciphertext, sign string) error { data := appID + timestamp + ciphertext return rsaVerify([]byte(data), sign, c.publicKey) } // cipherRequest 请求体结构 type cipherRequest struct { Ciphertext string `json:"ciphertext"` } // apiResponse 公共响应结构 type apiResponse struct { Code int `json:"code"` Message string `json:"message"` Reason string `json:"reason,omitempty"` Data *cipherData `json:"data,omitempty"` } type cipherData struct { Ciphertext string `json:"ciphertext"` }