// File: ymt_v3/go.mod ```go module ymt_v3 go 1.21 require github.com/tjfoc/gmsm v1.4.1 ``` // File: ymt_v3/types.go ```go package ymt_v3 // CommonResponse 公共响应结构 type CommonResponse struct { Code int32 `json:"code"` Message string `json:"message"` Reason string `json:"reason,omitempty"` Data *CommonData `json:"data,omitempty"` } // CommonData 公共响应数据(包含加密的ciphertext) type CommonData struct { Ciphertext string `json:"ciphertext"` } // KeyOrderRequest 获取券码请求业务参数 type KeyOrderRequest struct { OutBizNo string `json:"out_biz_no"` ActivityNo string `json:"activity_no"` Account string `json:"account,omitempty"` NotifyURL string `json:"notify_url,omitempty"` } // KeyOrderResponse 获取券码/券码查询响应业务参数 type KeyOrderResponse 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"` } // KeyQueryRequest 券码查询请求业务参数 type KeyQueryRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } // KeyDiscardRequest 券码作废请求业务参数 type KeyDiscardRequest struct { OutBizNo string `json:"out_biz_no,omitempty"` TradeNo string `json:"trade_no,omitempty"` } // KeyDiscardResponse 券码作废响应业务参数 type KeyDiscardResponse 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"` } // NotifyPayload 回调通知的body结构 type NotifyPayload struct { Data *CommonData `json:"data"` } // NotifyHeader 回调通知的header type NotifyHeader struct { Appid string `json:"Appid"` Timestamp string `json:"Timestamp"` Sign string `json:"Sign"` } ``` // File: ymt_v3/errors.go ```go package ymt_v3 import "fmt" // APIError 表示API返回的业务错误 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) } // IsSuccess 判断是否成功 func (e *APIError) IsSuccess() bool { return e.Code == 200 } // NewAPIError 创建API错误 func NewAPIError(code int32, message, reason string) *APIError { return &APIError{ Code: code, Message: message, Reason: reason, } } // SDKError SDK内部错误 type SDKError struct { Message string Err error } func (e *SDKError) Error() string { if e.Err != nil { return fmt.Sprintf("SDK error: %s: %v", e.Message, e.Err) } return fmt.Sprintf("SDK error: %s", e.Message) } func (e *SDKError) Unwrap() error { return e.Err } // NewSDKError 创建SDK内部错误 func NewSDKError(msg string, err error) *SDKError { return &SDKError{Message: msg, Err: err} } ``` // File: ymt_v3/crypto.go ```go package ymt_v3 import ( "bytes" "crypto" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" "reflect" "sort" "strings" "time" "github.com/tjfoc/gmsm/sm4" ) // EncryptType 加密类型 type EncryptType string const ( // EncryptTypeAES AES-ECB加密 EncryptTypeAES EncryptType = "aes" // EncryptTypeSM4 SM4-CBC加密 EncryptTypeSM4 EncryptType = "sm4" ) // ==================== RSA签名 ==================== // SignWithRSA 使用RSA私钥对数据进行签名 func SignWithRSA(signStr string, privateKeyPEM string) (string, error) { block, _ := pem.Decode([]byte(privateKeyPEM)) if block == nil { return "", fmt.Errorf("failed to decode PEM private key") } privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return "", fmt.Errorf("failed to parse private key: %v", err) } } rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey) if !ok { return "", fmt.Errorf("not a RSA private key") } h := sha256.New() h.Write([]byte(signStr)) hashed := h.Sum(nil) signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(signature), nil } // VerifyWithRSA 使用RSA公钥验证签名 func VerifyWithRSA(signStr, signature string, publicKeyPEM string) error { block, _ := pem.Decode([]byte(publicKeyPEM)) if block == nil { return fmt.Errorf("failed to decode PEM public key") } publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return fmt.Errorf("failed to parse public key: %v", err) } rsaPublicKey, ok := publicKey.(*rsa.PublicKey) if !ok { return fmt.Errorf("not a RSA public key") } h := sha256.New() h.Write([]byte(signStr)) hashed := h.Sum(nil) sigBytes, err := base64.StdEncoding.DecodeString(signature) if err != nil { return fmt.Errorf("failed to decode signature: %v", err) } return rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed, sigBytes) } // ==================== AES ECB加密 ==================== // AESECBEncrypt AES-ECB模式加密 func AESECBEncrypt(plaintext []byte, key []byte) (string, error) { block, err := aes.NewCipher(key) if err != nil { return "", fmt.Errorf("创建AES cipher失败: %v", err) } padded := pkcs7Padding(plaintext, aes.BlockSize) ciphertext := make([]byte, len(padded)) for i := 0; i < len(padded); i += aes.BlockSize { block.Encrypt(ciphertext[i:i+aes.BlockSize], padded[i:i+aes.BlockSize]) } return base64.StdEncoding.EncodeToString(ciphertext), nil } // AESECBDecrypt AES-ECB模式解密 func AESECBDecrypt(encryptedData string, key []byte) ([]byte, error) { ciphertext, err := base64.StdEncoding.DecodeString(encryptedData) if err != nil { return nil, fmt.Errorf("解码失败: %v", err) } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("创建AES cipher失败: %v", err) } if len(ciphertext)%aes.BlockSize != 0 { return nil, fmt.Errorf("密文长度不是块大小的整数倍") } plaintext := make([]byte, len(ciphertext)) for i := 0; i < len(ciphertext); i += aes.BlockSize { block.Decrypt(plaintext[i:i+aes.BlockSize], ciphertext[i:i+aes.BlockSize]) } plaintext, err = pkcs7UnPadding(plaintext) if err != nil { return nil, fmt.Errorf("去除填充失败: %v", err) } return plaintext, nil } // ==================== SM4 CBC加密 ==================== // SM4CBCEncrypt SM4-CBC模式加密 // IV会前置到密文中,解密时自动提取 func SM4CBCEncrypt(plaintext []byte, key []byte) (string, error) { if len(key) != 16 { return "", fmt.Errorf("SM4密钥长度必须为16字节") } block, err := sm4.NewCipher(key) if err != nil { return "", fmt.Errorf("创建SM4 cipher失败: %v", err) } padded := pkcs7Padding(plaintext, block.BlockSize()) iv := make([]byte, block.BlockSize()) if _, err := io.ReadFull(rand.Reader, iv); err != nil { return "", fmt.Errorf("生成IV失败: %v", err) } mode := cipher.NewCBCEncrypter(block, iv) ciphertext := make([]byte, len(padded)) mode.CryptBlocks(ciphertext, padded) // IV前置到密文 result := append(iv, ciphertext...) return base64.StdEncoding.EncodeToString(result), nil } // SM4CBCDecrypt SM4-CBC模式解密 // 从密文中提取前置的IV进行解密 func SM4CBCDecrypt(encryptedData string, key []byte) ([]byte, error) { if len(key) != 16 { return nil, fmt.Errorf("SM4密钥长度必须为16字节") } data, err := base64.StdEncoding.DecodeString(encryptedData) if err != nil { return nil, fmt.Errorf("解码失败: %v", err) } block, err := sm4.NewCipher(key) if err != nil { return nil, fmt.Errorf("创建SM4 cipher失败: %v", err) } blockSize := block.BlockSize() if len(data) < blockSize*2 { return nil, fmt.Errorf("数据长度不足") } iv := data[:blockSize] ciphertext := data[blockSize:] mode := cipher.NewCBCDecrypter(block, iv) plaintext := make([]byte, len(ciphertext)) mode.CryptBlocks(plaintext, ciphertext) plaintext, err = pkcs7UnPadding(plaintext) if err != nil { return nil, fmt.Errorf("去除填充失败: %v", err) } return plaintext, nil } // ==================== PKCS7填充 ==================== // pkcs7Padding PKCS7填充 func pkcs7Padding(data []byte, blockSize int) []byte { padding := blockSize - len(data)%blockSize padText := bytes.Repeat([]byte{byte(padding)}, padding) return append(data, padText...) } // pkcs7UnPadding 去除PKCS7填充 func pkcs7UnPadding(data []byte) ([]byte, error) { length := len(data) if length == 0 { return nil, fmt.Errorf("数据为空") } padding := int(data[length-1]) if padding > length || padding == 0 { return nil, fmt.Errorf("无效的填充") } for i := length - padding; i < length; i++ { if data[i] != byte(padding) { return nil, fmt.Errorf("无效的填充") } } return data[:length-padding], nil } // ==================== 业务参数加密/解密 ==================== // EncryptBizParams 加密业务参数 // 1. 将业务参数去掉"零"值的参数,再由小到大按照字母排序,转成json字符串得到plaintext // 2. 使用应用key将plaintext字符串加密,得到ciphertext func EncryptBizParams(params interface{}, key []byte, encType EncryptType) (string, error) { plaintext, err := marshalWithoutZero(params) if err != nil { return "", fmt.Errorf("序列化业务参数失败: %v", err) } switch encType { case EncryptTypeAES: return AESECBEncrypt([]byte(plaintext), key) case EncryptTypeSM4: return SM4CBCEncrypt([]byte(plaintext), key) default: return "", fmt.Errorf("不支持的加密类型: %s", encType) } } // DecryptBizParams 解密业务参数 func DecryptBizParams(ciphertext string, key []byte, encType EncryptType) ([]byte, error) { switch encType { case EncryptTypeAES: return AESECBDecrypt(ciphertext, key) case EncryptTypeSM4: return SM4CBCDecrypt(ciphertext, key) default: return nil, fmt.Errorf("不支持的加密类型: %s", encType) } } // EncryptBizParamsRaw 直接加密字节数据(用于回调验签) func EncryptBizParamsRaw(plaintext []byte, key []byte, encType EncryptType) (string, error) { switch encType { case EncryptTypeAES: return AESECBEncrypt(plaintext, key) case EncryptTypeSM4: return SM4CBCEncrypt(plaintext, key) default: return "", fmt.Errorf("不支持的加密类型: %s", encType) } } // marshalWithoutZero 将结构体序列化为JSON,去掉零值字段并按字母排序 func marshalWithoutZero(v interface{}) (string, error) { val := reflect.ValueOf(v) if val.Kind() == reflect.Ptr { val = val.Elem() } if val.Kind() != reflect.Struct { if val.Kind() == reflect.Map { // 类型断言为 map[string]interface{} m, ok := v.(map[string]interface{}) if !ok { // 尝试从 reflect 转换 m = make(map[string]interface{}) for _, key := range val.MapKeys() { m[fmt.Sprintf("%v", key.Interface())] = val.MapIndex(key).Interface() } } return marshalMapSorted(m) } data, err := json.Marshal(v) return string(data), err } result := make(map[string]interface{}) typ := val.Type() for i := 0; i < val.NumField(); i++ { field := val.Field(i) fieldType := typ.Field(i) jsonTag := fieldType.Tag.Get("json") if jsonTag == "" || jsonTag == "-" { continue } name := strings.Split(jsonTag, ",")[0] if isZeroValue(field) { continue } result[name] = field.Interface() } return marshalMapSorted(result) } // marshalMapSorted 将map按键排序后序列化为JSON func marshalMapSorted(m map[string]interface{}) (string, error) { keys := make([]string, 0, len(m)) for k := range m { 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(m[k]) if err != nil { return "", err } buf.Write(valBytes) } buf.WriteByte('}') return buf.String(), nil } // isZeroValue 判断reflect.Value是否为零值 func isZeroValue(v reflect.Value) bool { switch v.Kind() { case reflect.String: return v.String() == "" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Bool: return !v.Bool() case reflect.Slice, reflect.Map, reflect.Ptr: return v.IsNil() default: return false } } // ==================== 签名相关 ==================== // BuildSignStr 构建签名字符串 // 规则:app_id + timestamp + ciphertext func BuildSignStr(appID, timestamp, ciphertext string) string { return appID + timestamp + ciphertext } // GenerateTimestamp 生成时间戳,格式 yyyy-MM-dd HH:mm:ss func GenerateTimestamp() string { return time.Now().Format("2006-01-02 15:04:05") } // ==================== 回调验签 ==================== // VerifyNotifySign 验证回调通知签名 func VerifyNotifySign(header *NotifyHeader, data interface{}, key []byte, publicKeyPEM string, encType EncryptType) error { plaintext, err := marshalWithoutZero(data) if err != nil { return fmt.Errorf("序列化回调数据失败: %v", err) } ciphertext, err := EncryptBizParamsRaw([]byte(plaintext), key, encType) if err != nil { return fmt.Errorf("加密回调数据失败: %v", err) } signStr := BuildSignStr(header.Appid, header.Timestamp, ciphertext) return VerifyWithRSA(signStr, header.Sign, publicKeyPEM) } ``` // File: ymt_v3/client.go ```go 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) } ``` // File: ymt_v3/example_test.go ```go package ymt_v3 import ( "encoding/json" "fmt" "testing" ) // 测试参数(请替换为实际值) const ( testAppID = "your_app_id" testPrivateKey = `-----BEGIN PRIVATE KEY----- your_private_key_here -----END PRIVATE KEY-----` testPublicKey = `-----BEGIN PUBLIC KEY----- your_public_key_here -----END PUBLIC KEY-----` testKey = "your_encryption_key_16" testActivityNo = "your_activity_no" ) func ExampleClient_KeyOrder() { // 创建客户端 client, err := NewClient(&ClientConfig{ AppID: testAppID, PrivateKey: testPrivateKey, PublicKey: testPublicKey, Key: testKey, EncryptType: EncryptTypeAES, Env: EnvTest, }) if err != nil { fmt.Printf("创建客户端失败: %v\n", err) return } // 获取券码 resp, err := client.KeyOrder(&KeyOrderRequest{ OutBizNo: "order_001", ActivityNo: testActivityNo, Account: "18666666666", NotifyURL: "https://notify.example.com/openapi", }) if err != nil { fmt.Printf("获取券码失败: %v\n", err) return } fmt.Printf("交易号: %s\n", resp.TradeNo) fmt.Printf("卡密: %s\n", resp.Key) fmt.Printf("状态: %d\n", resp.Status) // Output: // 交易号: 7251449503000383488 // 卡密: aZKdU9BymzR6qGRzJM // 状态: 1 } func ExampleClient_KeyQuery() { client, _ := NewClient(&ClientConfig{ AppID: testAppID, PrivateKey: testPrivateKey, PublicKey: testPublicKey, Key: testKey, EncryptType: EncryptTypeAES, Env: EnvTest, }) // 按外部业务号查询 resp, err := client.KeyQuery(&KeyQueryRequest{ OutBizNo: "order_001", }) if err != nil { fmt.Printf("查询失败: %v\n", err) return } fmt.Printf("交易号: %s\n", resp.TradeNo) fmt.Printf("状态: %d\n", resp.Status) } func ExampleClient_KeyDiscard() { client, _ := NewClient(&ClientConfig{ AppID: testAppID, PrivateKey: testPrivateKey, PublicKey: testPublicKey, Key: testKey, EncryptType: EncryptTypeAES, Env: EnvTest, }) // 按交易号作废 resp, err := client.KeyDiscard(&KeyDiscardRequest{ TradeNo: "7251449503000383488", }) if err != nil { fmt.Printf("作废失败: %v\n", err) return } fmt.Printf("交易号: %s\n", resp.TradeNo) fmt.Printf("状态: %d\n", resp.Status) } func ExampleClient_BatchOrder() { client, _ := NewClient(&ClientConfig{ AppID: testAppID, PrivateKey: testPrivateKey, PublicKey: testPublicKey, Key: testKey, EncryptType: EncryptTypeAES, Env: EnvTest, }) // 批量发卡 resp, err := client.BatchOrder(&BatchOrderRequest{ OutBizNo: "batch_001", ActivityNo: testActivityNo, Number: 100, NotifyURL: "https://notify.example.com/openapi", }) if err != nil { fmt.Printf("批量发卡失败: %v\n", err) return } fmt.Printf("交易号: %s\n", resp.TradeNo) fmt.Printf("状态: %s\n", resp.Status) } func ExampleClient_BatchQuery() { client, _ := NewClient(&ClientConfig{ AppID: testAppID, PrivateKey: testPrivateKey, PublicKey: testPublicKey, Key: testKey, EncryptType: EncryptTypeAES, Env: EnvTest, }) // 查询批量任务 resp, err := client.BatchQuery(&BatchQueryRequest{ TradeNo: "7251449503000383499", }) if err != nil { fmt.Printf("查询失败: %v\n", err) return } fmt.Printf("状态: %s\n", resp.Status) if resp.DownloadURL != "" { fmt.Printf("下载地址: %s\n", resp.DownloadURL) } } func TestMarshalWithoutZero(t *testing.T) { req := KeyOrderRequest{ OutBizNo: "order_001", ActivityNo: "ACT20260622001", // Account和NotifyURL为空,应该被去掉 } result, err := marshalWithoutZero(req) if err != nil { t.Fatalf("marshalWithoutZero失败: %v", err) } expected := `{"activity_no":"ACT20260622001","out_biz_no":"order_001"}` if result != expected { t.Errorf("期望 %s, 得到 %s", expected, result) } } func TestBuildSignStr(t *testing.T) { appID := "123456" timestamp := "2026-06-22 15:30:00" ciphertext := "encrypted_data" signStr := BuildSignStr(appID, timestamp, ciphertext) expected := "1234562026-06-22 15:30:00encrypted_data" if signStr != expected { t.Errorf("期望 %s, 得到 %s", expected, signStr) } } func TestGenerateTimestamp(t *testing.T) { ts := GenerateTimestamp() // 验证格式 yyyy-MM-dd HH:mm:ss if len(ts) != 19 { t.Errorf("时间戳格式错误: %s", ts) } } func TestEncryptDecryptAES(t *testing.T) { key := []byte("1234567890123456") // 16字节 plaintext := `{"out_biz_no":"order_001","activity_no":"ACT001"}` ciphertext, err := AESECBEncrypt([]byte(plaintext), key) if err != nil { t.Fatalf("加密失败: %v", err) } decrypted, err := AESECBDecrypt(ciphertext, key) if err != nil { t.Fatalf("解密失败: %v", err) } if string(decrypted) != plaintext { t.Errorf("解密结果不匹配: 期望 %s, 得到 %s", plaintext, string(decrypted)) } } func TestEncryptDecryptSM4(t *testing.T) { key := []byte("1234567890123456") // 16字节 plaintext := `{"out_biz_no":"order_001","activity_no":"ACT001"}` ciphertext, err := SM4CBCEncrypt([]byte(plaintext), key) if err != nil { t.Fatalf("SM4加密失败: %v", err) } decrypted, err := SM4CBCDecrypt(ciphertext, key) if err != nil { t.Fatalf("SM4解密失败: %v", err) } if string(decrypted) != plaintext { t.Errorf("SM4解密结果不匹配: 期望 %s, 得到 %s", plaintext, string(decrypted)) } } func TestEncryptBizParams(t *testing.T) { key := []byte("1234567890123456") req := KeyOrderRequest{ OutBizNo: "order_001", ActivityNo: "ACT20260622001", } ciphertext, err := EncryptBizParams(req, key, EncryptTypeAES) if err != nil { t.Fatalf("加密业务参数失败: %v", err) } if ciphertext == "" { t.Fatal("加密结果为空") } // 验证可以解密 decrypted, err := DecryptBizParams(ciphertext, key, EncryptTypeAES) if err != nil { t.Fatalf("解密失败: %v", err) } var result KeyOrderRequest if err := json.Unmarshal(decrypted, &result); err != nil { t.Fatalf("解析失败: %v", err) } if result.OutBizNo != "order_001" { t.Errorf("OutBizNo不匹配: 期望 order_001, 得到 %s", result.OutBizNo) } } func TestNewClient(t *testing.T) { _, err := NewClient(&ClientConfig{ AppID: testAppID, PrivateKey: testPrivateKey, PublicKey: testPublicKey, Key: testKey, EncryptType: EncryptTypeAES, Env: EnvTest, }) if err != nil { t.Fatalf("创建客户端失败: %v", err) } } func TestNewClientMissingFields(t *testing.T) { tests := []struct { name string config *ClientConfig wantErr bool }{ {"空AppID", &ClientConfig{PrivateKey: "pk", PublicKey: "pub", Key: "key"}, true}, {"空PrivateKey", &ClientConfig{AppID: "app", PublicKey: "pub", Key: "key"}, true}, {"空PublicKey", &ClientConfig{AppID: "app", PrivateKey: "pk", Key: "key"}, true}, {"空Key", &ClientConfig{AppID: "app", PrivateKey: "pk", PublicKey: "pub"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := NewClient(tt.config) if (err != nil) != tt.wantErr { t.Errorf("NewClient() error = %v, wantErr = %v", err, tt.wantErr) } }) } } func TestAPIError(t *testing.T) { err := NewAPIError(401, "Signature verification failed", "INVALID_SIGNATURE") if err.Code != 401 { t.Errorf("Code不匹配: 期望 401, 得到 %d", err.Code) } if err.Message != "Signature verification failed" { t.Errorf("Message不匹配") } if err.Reason != "INVALID_SIGNATURE" { t.Errorf("Reason不匹配") } if err.Error() != "API error: code=401, message=Signature verification failed, reason=INVALID_SIGNATURE" { t.Errorf("Error()不匹配: %s", err.Error()) } if err.IsSuccess() { t.Error("401应该不是成功") } } func TestSDKError(t *testing.T) { err := NewSDKError("测试错误", nil) if err.Error() != "SDK error: 测试错误" { t.Errorf("Error()不匹配: %s", err.Error()) } } ```