// 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" ) type Client struct { httpClient *http.Client baseURL string appID string privateKey *rsa.PrivateKey publicKey *rsa.PublicKey aesKey []byte signType string } type Option func(*Client) func WithBaseURL(url string) Option { return func(c *Client) { c.baseURL = url } } func WithTimeout(d time.Duration) Option { return func(c *Client) { c.httpClient.Timeout = d } } func WithHTTPClient(client *http.Client) Option { return func(c *Client) { c.httpClient = client } } func WithAppID(appID string) Option { return func(c *Client) { c.appID = appID } } 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 } } 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 } } func WithAESKey(key string) Option { return func(c *Client) { c.aesKey = []byte(key) } } func WithSignType(signType string) Option { return func(c *Client) { c.signType = signType } } 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 } func (c *Client) Do(ctx context.Context, method, path string, bizReq interface{}, bizResp interface{}) error { plaintext, err := marshalSortedNoZero(bizReq) if err != nil { return fmt.Errorf("marshal request: %w", err) } ciphertext, err := aesEncryptECB(plaintext, c.aesKey) if err != nil { return fmt.Errorf("encrypt: %w", err) } timestamp := time.Now().Format("2006-01-02 15:04:05") signStr := c.appID + timestamp + ciphertext sign, err := rsaSign([]byte(signStr), c.privateKey) if err != nil { return fmt.Errorf("sign: %w", err) } reqBody := cipherRequest{Ciphertext: ciphertext} bodyBytes, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("marshal request body: %w", err) } 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") 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) } 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, } } if len(commonResp.Data) == 0 { return nil } var cd cipherData if err := json.Unmarshal(commonResp.Data, &cd); err == nil && cd.Ciphertext != "" { decrypted, err := aesDecryptECB(cd.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) } } else { if err := json.Unmarshal(commonResp.Data, bizResp); err != nil { return fmt.Errorf("unmarshal plain data: %w", err) } } return nil } type cipherRequest struct { Ciphertext string `json:"ciphertext"` } type commonResponse struct { Code int32 `json:"code"` Message string `json:"message"` Reason string `json:"reason,omitempty"` Data json.RawMessage `json:"data,omitempty"` } type cipherData struct { Ciphertext string `json:"ciphertext"` } ``` // File: ymt_v3_2/types.go ```go package ymt_v3_2 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"` } 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"` } type QueryKeyRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } type QueryKeyResponse = GetKeyResponse type DiscardKeyRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } type DiscardKeyResponse struct { OutBizNo string `json:"out_biz_no"` TradeNo string `json:"trade_no"` Status uint32 `json:"status"` } type BatchOrderRequest struct { OutBizNo string `json:"out_biz_no"` ActivityNo string `json:"activity_no"` Number int32 `json:"number"` NotifyURL string `json:"notify_url,omitempty"` } type BatchOrderResponse struct { OutBizNo string `json:"out_biz_no"` TradeNo string `json:"trade_no"` Status string `json:"status"` } type BatchQueryRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } 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" ) 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 { 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 } 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 } func marshalSortedNoZero(v interface{}) ([]byte, error) { 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) 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 nil, err } buf.Write(valBytes) } buf.WriteByte('}') return buf.Bytes(), nil } func removeZeroValues(m map[string]interface{}) map[string]interface{} { res := make(map[string]interface{}) for k, v := range m { if isZeroValue(v) { continue } if sub, ok := v.(map[string]interface{}); ok { subFiltered := removeZeroValues(sub) if len(subFiltered) > 0 { res[k] = subFiltered } } else { res[k] = v } } return res } 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 } } 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)) for i := 0; i < len(plaintext); i += blockSize { block.Encrypt(ciphertext[i:i+blockSize], plaintext[i:i+blockSize]) } return base64.StdEncoding.EncodeToString(ciphertext), nil } 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 } func pkcs7Padding(data []byte, blockSize int) []byte { padding := blockSize - len(data)%blockSize padtext := bytes.Repeat([]byte{byte(padding)}, padding) return append(data, padtext...) } 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 } 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 } 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) } 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) } var _ = cipher.NewCBCEncrypter var _ = strings.ReplaceAll ``` // File: ymt_v3_2/errors.go ```go package ymt_v3_2 import "fmt" type APIError struct { Code int32 `json:"code"` Message string `json:"message"` Reason string `json:"reason,omitempty"` } 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" 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 } 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 } 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 } 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 } 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) } ```