xy_sh-20260727101126/xy_sh/ymt_v3/valid.md

1149 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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())
}
}
```