添加文件: ymt_v3_api/client.go
This commit is contained in:
parent
91702f243c
commit
db35f19def
|
|
@ -0,0 +1,229 @@
|
|||
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"`
|
||||
}
|
||||
Loading…
Reference in New Issue