29 KiB
29 KiB
// File: ymt_v3/go.mod
module ymt_v3
go 1.21
require github.com/tjfoc/gmsm v1.4.1
// File: ymt_v3/types.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
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
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
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
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())
}
}