// File: ymt_v3_2/go.mod ```go module ymt_v3_2 go 1.21 ``` // File: ymt_v3_2/client.go ```go package ymt_v3_2 import ( "bytes" "context" "crypto/rsa" "encoding/json" "fmt" "io" "net/http" "time" ) // Client 是 SDK 的客户端,封装了 HTTP 通信、加密、签名等逻辑。 type Client struct { httpClient *http.Client baseURL string appID string privateKey *rsa.PrivateKey publicKey *rsa.PublicKey aesKey []byte signType string } // Option 是配置 Client 的函数选项。 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 } } // WithAppID 设置应用 ID。 func WithAppID(appID string) Option { return func(c *Client) { c.appID = appID } } // WithPrivateKeyPEM 设置应用私钥(PEM 格式字符串)。 func WithPrivateKeyPEM(pemData string) Option { return func(c *Client) { key, err := parsePrivateKey(pemData) if err != nil { panic(fmt.Sprintf("failed to parse private key: %v", err)) } c.privateKey = key } } // WithPublicKeyPEM 设置平台公钥(PEM 格式字符串),用于验签。 func WithPublicKeyPEM(pemData string) Option { return func(c *Client) { key, err := parsePublicKey(pemData) if err != nil { panic(fmt.Sprintf("failed to parse public key: %v", err)) } c.publicKey = key } } // WithAESKey 设置业务参数加解密密钥(AES 密钥,16/24/32 字节)。 func WithAESKey(key string) Option { return func(c *Client) { c.aesKey = []byte(key) } } // WithSignType 设置签名类型,目前仅支持 "RSA"。 func WithSignType(signType string) Option { return func(c *Client) { c.signType = signType } } // NewClient 创建一个新的客户端实例。 func NewClient(opts ...Option) *Client { c := &Client{ httpClient: &http.Client{Timeout: 30 * time.Second}, baseURL: "https://gateway.dev.cdlsxd.cn", signType: "RSA", } for _, opt := range opts { opt(c) } return c } // Do 执行一个 API 请求,自动处理加密、签名、解密。 // bizReq 为业务请求结构体,bizResp 为业务响应结构体指针。 func (c *Client) Do(ctx context.Context, method, path string, bizReq interface{}, bizResp interface{}) error { // 1. 业务参数过滤零值、排序、序列化 plaintext, err := marshalSortedNoZero(bizReq) if err != nil { return fmt.Errorf("marshal request: %w", err) } // 2. AES 加密 ciphertext, err := aesEncryptECB(plaintext, c.aesKey) if err != nil { return fmt.Errorf("encrypt: %w", err) } // 3. 生成时间戳 timestamp := time.Now().Format("2006-01-02 15:04:05") // 4. 签名 signStr := c.appID + timestamp + ciphertext sign, err := rsaSign([]byte(signStr), c.privateKey) if err != nil { return fmt.Errorf("sign: %w", err) } // 5. 构造请求体 reqBody := cipherRequest{Ciphertext: ciphertext} bodyBytes, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("marshal request body: %w", err) } // 6. 创建 HTTP 请求 url := c.baseURL + path req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(bodyBytes)) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("Appid", c.appID) req.Header.Set("Timestamp", timestamp) req.Header.Set("Sign", sign) req.Header.Set("Content-Type", "application/json") // 7. 发送请求 resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("http request: %w", err) } defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("read response: %w", err) } // 8. 解析公共响应 var commonResp commonResponse if err := json.Unmarshal(respBytes, &commonResp); err != nil { return fmt.Errorf("unmarshal response: %w", err) } if commonResp.Code != 200 { return &APIError{ Code: commonResp.Code, Message: commonResp.Message, Reason: commonResp.Reason, } } // 9. 解密业务数据 if commonResp.Data == nil || commonResp.Data.Ciphertext == "" { // 某些接口可能直接返回 data 中的明文(如查询接口文档示例中 ciphertext 为空) // 此时尝试直接解析整个 data 到 bizResp if commonResp.Data != nil { // 如果 data 中有其他字段,尝试直接反序列化 data dataBytes, _ := json.Marshal(commonResp.Data) if err := json.Unmarshal(dataBytes, bizResp); err != nil { return fmt.Errorf("unmarshal plain data: %w", err) } return nil } return nil } // 解密 ciphertext decrypted, err := aesDecryptECB(commonResp.Data.Ciphertext, c.aesKey) if err != nil { return fmt.Errorf("decrypt response: %w", err) } if err := json.Unmarshal(decrypted, bizResp); err != nil { return fmt.Errorf("unmarshal decrypted response: %w", err) } return nil } // cipherRequest 请求体结构 type cipherRequest struct { Ciphertext string `json:"ciphertext"` } // commonResponse 公共响应结构 type commonResponse struct { Code int32 `json:"code"` Message string `json:"message"` Reason string `json:"reason,omitempty"` Data *cipherData `json:"data,omitempty"` } // cipherData 加密数据载体 type cipherData struct { Ciphertext string `json:"ciphertext"` } ``` // File: ymt_v3_2/types.go ```go package ymt_v3_2 // GetKeyRequest 获取券码请求参数 type GetKeyRequest struct { OutBizNo string `json:"out_biz_no"` ActivityNo string `json:"activity_no"` Account string `json:"account,omitempty"` NotifyURL string `json:"notify_url,omitempty"` } // GetKeyResponse 获取券码响应参数 type GetKeyResponse struct { OutBizNo string `json:"out_biz_no"` TradeNo string `json:"trade_no"` Key string `json:"key,omitempty"` URL string `json:"url,omitempty"` ValidBeginTime string `json:"valid_begin_time,omitempty"` ValidEndTime string `json:"valid_end_time,omitempty"` UsableNum uint32 `json:"usable_num"` UsageNum uint32 `json:"usage_num"` Status uint32 `json:"status"` SettlementPrice float64 `json:"settlement_price,omitempty"` Account string `json:"account,omitempty"` } // QueryKeyRequest 券码查询请求参数 type QueryKeyRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } // QueryKeyResponse 券码查询响应参数(与获取券码一致) type QueryKeyResponse = GetKeyResponse // DiscardKeyRequest 券码作废请求参数 type DiscardKeyRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } // DiscardKeyResponse 券码作废响应参数 type DiscardKeyResponse struct { OutBizNo string `json:"out_biz_no"` TradeNo string `json:"trade_no"` Status uint32 `json:"status"` } // BatchOrderRequest 批量发卡请求参数 type BatchOrderRequest struct { OutBizNo string `json:"out_biz_no"` ActivityNo string `json:"activity_no"` Number int32 `json:"number"` NotifyURL string `json:"notify_url,omitempty"` } // BatchOrderResponse 批量发卡响应参数 type BatchOrderResponse struct { OutBizNo string `json:"out_biz_no"` TradeNo string `json:"trade_no"` Status string `json:"status"` } // BatchQueryRequest 批量查询请求参数 type BatchQueryRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } // BatchQueryResponse 批量查询响应参数 type BatchQueryResponse struct { OutBizNo string `json:"out_biz_no"` TradeNo string `json:"trade_no"` Status string `json:"status"` DownloadURL string `json:"download_url,omitempty"` ZipPassword string `json:"zip_password,omitempty"` } ``` // File: ymt_v3_2/crypto.go ```go package ymt_v3_2 import ( "bytes" "crypto" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "errors" "fmt" "sort" "strings" ) // parsePrivateKey 解析 PEM 格式的 RSA 私钥。 func parsePrivateKey(pemData string) (*rsa.PrivateKey, error) { block, _ := pem.Decode([]byte(pemData)) if block == nil { return nil, errors.New("failed to decode PEM block") } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { // 尝试 PKCS1 key, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("parse private key: %w", err) } } rsaKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, errors.New("not an RSA private key") } return rsaKey, nil } // parsePublicKey 解析 PEM 格式的 RSA 公钥。 func parsePublicKey(pemData string) (*rsa.PublicKey, error) { block, _ := pem.Decode([]byte(pemData)) if block == nil { return nil, errors.New("failed to decode PEM block") } key, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, fmt.Errorf("parse public key: %w", err) } rsaKey, ok := key.(*rsa.PublicKey) if !ok { return nil, errors.New("not an RSA public key") } return rsaKey, nil } // marshalSortedNoZero 将结构体转为 map,过滤零值,按 key 排序后序列化为 JSON 字符串。 func marshalSortedNoZero(v interface{}) ([]byte, error) { // 先转为 map data, err := json.Marshal(v) if err != nil { return nil, err } var raw map[string]interface{} if err := json.Unmarshal(data, &raw); err != nil { return nil, err } // 过滤零值 filtered := removeZeroValues(raw) // 按 key 排序 keys := make([]string, 0, len(filtered)) for k := range filtered { keys = append(keys, k) } sort.Strings(keys) // 构建有序的 JSON 字符串 var buf bytes.Buffer buf.WriteByte('{') for i, k := range keys { if i > 0 { buf.WriteByte(',') } keyBytes, _ := json.Marshal(k) buf.Write(keyBytes) buf.WriteByte(':') valBytes, err := json.Marshal(filtered[k]) if err != nil { return nil, err } buf.Write(valBytes) } buf.WriteByte('}') return buf.Bytes(), nil } // removeZeroValues 递归移除 map 中的零值(nil, 空字符串, 0, false, 空切片/数组, 空 map)。 func removeZeroValues(m map[string]interface{}) map[string]interface{} { res := make(map[string]interface{}) for k, v := range m { if isZeroValue(v) { continue } // 递归处理嵌套 map if sub, ok := v.(map[string]interface{}); ok { subFiltered := removeZeroValues(sub) if len(subFiltered) > 0 { res[k] = subFiltered } } else { res[k] = v } } return res } // isZeroValue 判断值是否为零值。 func isZeroValue(v interface{}) bool { if v == nil { return true } switch val := v.(type) { case string: return val == "" case float64: return val == 0 case bool: return !val case []interface{}: return len(val) == 0 case map[string]interface{}: return len(val) == 0 default: return false } } // aesEncryptECB 使用 AES ECB 模式加密,PKCS7 填充。 func aesEncryptECB(plaintext, key []byte) (string, error) { block, err := aes.NewCipher(key) if err != nil { return "", err } blockSize := block.BlockSize() plaintext = pkcs7Padding(plaintext, blockSize) ciphertext := make([]byte, len(plaintext)) // ECB 模式:逐块加密 for i := 0; i < len(plaintext); i += blockSize { block.Encrypt(ciphertext[i:i+blockSize], plaintext[i:i+blockSize]) } return base64.StdEncoding.EncodeToString(ciphertext), nil } // aesDecryptECB 使用 AES ECB 模式解密,PKCS7 去填充。 func aesDecryptECB(ciphertext string, key []byte) ([]byte, error) { data, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, err } block, err := aes.NewCipher(key) if err != nil { return nil, err } blockSize := block.BlockSize() if len(data)%blockSize != 0 { return nil, errors.New("ciphertext is not a multiple of the block size") } plaintext := make([]byte, len(data)) for i := 0; i < len(data); i += blockSize { block.Decrypt(plaintext[i:i+blockSize], data[i:i+blockSize]) } plaintext, err = pkcs7Unpadding(plaintext, blockSize) if err != nil { return nil, err } return plaintext, nil } // pkcs7Padding 填充。 func pkcs7Padding(data []byte, blockSize int) []byte { padding := blockSize - len(data)%blockSize padtext := bytes.Repeat([]byte{byte(padding)}, padding) return append(data, padtext...) } // pkcs7Unpadding 去填充。 func pkcs7Unpadding(data []byte, blockSize int) ([]byte, error) { length := len(data) if length == 0 { return nil, errors.New("empty data") } padding := int(data[length-1]) if padding > blockSize || padding == 0 { return nil, errors.New("invalid padding") } for i := length - padding; i < length; i++ { if data[i] != byte(padding) { return nil, errors.New("invalid padding") } } return data[:length-padding], nil } // rsaSign 使用 RSA 私钥对数据进行 SHA256 签名,返回 Base64 编码的签名。 func rsaSign(data []byte, privateKey *rsa.PrivateKey) (string, error) { hashed := sha256.Sum256(data) signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:]) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(signature), nil } // rsaVerify 使用 RSA 公钥验证签名。 func rsaVerify(data []byte, sign string, publicKey *rsa.PublicKey) error { signBytes, err := base64.StdEncoding.DecodeString(sign) if err != nil { return err } hashed := sha256.Sum256(data) return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signBytes) } // VerifyCallbackSign 用于回调验签:根据回调数据、签名、appID、timestamp 验证签名。 // callbackData 为回调 body 中的 data 字段(JSON 对象),需要过滤零值排序后加密再验签。 func (c *Client) VerifyCallbackSign(appID, timestamp, sign string, callbackData map[string]interface{}) error { // 过滤零值并排序 filtered := removeZeroValues(callbackData) keys := make([]string, 0, len(filtered)) for k := range filtered { keys = append(keys, k) } sort.Strings(keys) var buf bytes.Buffer buf.WriteByte('{') for i, k := range keys { if i > 0 { buf.WriteByte(',') } keyBytes, _ := json.Marshal(k) buf.Write(keyBytes) buf.WriteByte(':') valBytes, err := json.Marshal(filtered[k]) if err != nil { return err } buf.Write(valBytes) } buf.WriteByte('}') plaintext := buf.Bytes() // 加密 ciphertext, err := aesEncryptECB(plaintext, c.aesKey) if err != nil { return err } // 拼接签名字符串 signStr := appID + timestamp + ciphertext return rsaVerify([]byte(signStr), sign, c.publicKey) } // 确保 crypto 包被使用(避免未使用导入的编译错误,实际已被使用) var _ = cipher.NewCBCEncrypter var _ = strings.ReplaceAll ``` // File: ymt_v3_2/errors.go ```go package ymt_v3_2 import "fmt" // APIError 表示 API 返回的业务错误。 type APIError struct { Code int32 `json:"code"` Message string `json:"message"` Reason string `json:"reason,omitempty"` } // Error 实现 error 接口。 func (e *APIError) Error() string { return fmt.Sprintf("API error: code=%d, message=%s, reason=%s", e.Code, e.Message, e.Reason) } ``` // File: ymt_v3_2/api_key.go ```go package ymt_v3_2 import "context" // GetKey 获取券码 func (c *Client) GetKey(ctx context.Context, req *GetKeyRequest) (*GetKeyResponse, error) { var resp GetKeyResponse err := c.Do(ctx, "POST", "/openapi/v1/key/order", req, &resp) if err != nil { return nil, err } return &resp, nil } // QueryKey 券码查询 func (c *Client) QueryKey(ctx context.Context, req *QueryKeyRequest) (*QueryKeyResponse, error) { var resp QueryKeyResponse err := c.Do(ctx, "POST", "/openapi/v1/key/query", req, &resp) if err != nil { return nil, err } return &resp, nil } // DiscardKey 券码作废 func (c *Client) DiscardKey(ctx context.Context, req *DiscardKeyRequest) (*DiscardKeyResponse, error) { var resp DiscardKeyResponse err := c.Do(ctx, "POST", "/openapi/v1/key/discard", req, &resp) if err != nil { return nil, err } return &resp, nil } // BatchOrder 批量发卡 func (c *Client) BatchOrder(ctx context.Context, req *BatchOrderRequest) (*BatchOrderResponse, error) { var resp BatchOrderResponse err := c.Do(ctx, "POST", "/openapi/v1/key/batch_order", req, &resp) if err != nil { return nil, err } return &resp, nil } // BatchQuery 批量查询 func (c *Client) BatchQuery(ctx context.Context, req *BatchQueryRequest) (*BatchQueryResponse, error) { var resp BatchQueryResponse err := c.Do(ctx, "POST", "/openapi/v1/key/batch_query", req, &resp) if err != nil { return nil, err } return &resp, nil } ``` // File: ymt_v3_2/example_test.go ```go package ymt_v3_2_test import ( "context" "fmt" "testing" "time" "ymt_v3_2" ) // 测试参数,请替换为真实值 const ( testAppID = "xxx" testPrivateKey = `-----BEGIN PRIVATE KEY----- xxx -----END PRIVATE KEY-----` testPublicKey = `-----BEGIN PUBLIC KEY----- xxx -----END PUBLIC KEY-----` testAESKey = "xxxx" testActivityNo = "xxxx" ) func newTestClient() *ymt_v3_2.Client { return ymt_v3_2.NewClient( ymt_v3_2.WithBaseURL("https://gateway.dev.cdlsxd.cn"), ymt_v3_2.WithAppID(testAppID), ymt_v3_2.WithPrivateKeyPEM(testPrivateKey), ymt_v3_2.WithPublicKeyPEM(testPublicKey), ymt_v3_2.WithAESKey(testAESKey), ymt_v3_2.WithTimeout(10*time.Second), ) } func TestGetKey(t *testing.T) { client := newTestClient() ctx := context.Background() req := &ymt_v3_2.GetKeyRequest{ OutBizNo: fmt.Sprintf("test_%d", time.Now().UnixNano()), ActivityNo: testActivityNo, Account: "18666666666", NotifyURL: "https://notify.example.com/openapi", } resp, err := client.GetKey(ctx, req) if err != nil { t.Fatalf("GetKey failed: %v", err) } t.Logf("GetKey response: %+v", resp) } func TestQueryKey(t *testing.T) { client := newTestClient() ctx := context.Background() req := &ymt_v3_2.QueryKeyRequest{ OutBizNo: "order_001", // 替换为真实的外部业务号 } resp, err := client.QueryKey(ctx, req) if err != nil { t.Fatalf("QueryKey failed: %v", err) } t.Logf("QueryKey response: %+v", resp) } func TestDiscardKey(t *testing.T) { client := newTestClient() ctx := context.Background() req := &ymt_v3_2.DiscardKeyRequest{ TradeNo: "7251449503000383488", // 替换为真实的交易号 } resp, err := client.DiscardKey(ctx, req) if err != nil { t.Fatalf("DiscardKey failed: %v", err) } t.Logf("DiscardKey response: %+v", resp) } func TestBatchOrder(t *testing.T) { client := newTestClient() ctx := context.Background() req := &ymt_v3_2.BatchOrderRequest{ OutBizNo: fmt.Sprintf("batch_%d", time.Now().UnixNano()), ActivityNo: testActivityNo, Number: 10, NotifyURL: "https://notify.example.com/openapi", } resp, err := client.BatchOrder(ctx, req) if err != nil { t.Fatalf("BatchOrder failed: %v", err) } t.Logf("BatchOrder response: %+v", resp) } func TestBatchQuery(t *testing.T) { client := newTestClient() ctx := context.Background() req := &ymt_v3_2.BatchQueryRequest{ OutBizNo: "batch_001", // 替换为真实的外部业务号 } resp, err := client.BatchQuery(ctx, req) if err != nil { t.Fatalf("BatchQuery failed: %v", err) } t.Logf("BatchQuery response: %+v", resp) } ```