ymt_v3_generate-20260721-10.../ymt_v3_generate/res.md

859 lines
22 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_generate/go.mod
```go
module ymt_v3_generate
go 1.21
```
// File: ymt_v3_generate/errors.go
```go
package ymt_v3_generate
import "fmt"
// APIError 表示 API 返回的业务错误
type APIError struct {
Code int `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_generate/types.go
```go
package ymt_v3_generate
// 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"`
}
// KeyQueryResponse 券码查询响应(与 KeyOrderResponse 一致)
type KeyQueryResponse = KeyOrderResponse
// 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"`
}
// ApiResponse 公共响应结构
type ApiResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Reason string `json:"reason,omitempty"`
Data struct {
Ciphertext string `json:"ciphertext"`
} `json:"data"`
}
```
// File: ymt_v3_generate/crypto.go
```go
package ymt_v3_generate
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"sort"
"strings"
)
// PKCS7Padding 填充
func pkcs7Padding(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padtext...)
}
// pkcs7UnPadding 去除填充
func pkcs7UnPadding(data []byte) ([]byte, error) {
length := len(data)
if length == 0 {
return nil, errors.New("data is empty")
}
unpadding := int(data[length-1])
if unpadding > length || unpadding == 0 {
return nil, errors.New("invalid padding")
}
return data[:length-unpadding], nil
}
// aesECBEncrypt AES ECB 加密
func aesECBEncrypt(plaintext []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
plaintext = pkcs7Padding(plaintext, blockSize)
ciphertext := make([]byte, len(plaintext))
// ECB 模式:逐块加密
for start := 0; start < len(plaintext); start += blockSize {
block.Encrypt(ciphertext[start:start+blockSize], plaintext[start:start+blockSize])
}
return ciphertext, nil
}
// aesECBDecrypt AES ECB 解密
func aesECBDecrypt(ciphertext []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
if len(ciphertext)%blockSize != 0 {
return nil, errors.New("ciphertext is not a multiple of block size")
}
plaintext := make([]byte, len(ciphertext))
for start := 0; start < len(ciphertext); start += blockSize {
block.Decrypt(plaintext[start:start+blockSize], ciphertext[start:start+blockSize])
}
return pkcs7UnPadding(plaintext)
}
// EncryptPlaintext 加密明文AES ECB返回 base64 字符串
func EncryptPlaintext(plaintext string, key []byte) (string, error) {
ciphertext, err := aesECBEncrypt([]byte(plaintext), key)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// DecryptCiphertext 解密密文base64 输入),返回明文字符串
func DecryptCiphertext(ciphertextBase64 string, key []byte) (string, error) {
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)
if err != nil {
return "", err
}
plaintext, err := aesECBDecrypt(ciphertext, key)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// Sign 生成签名appid + timestamp + ciphertext使用 RSA 私钥签名,返回 base64 签名
func Sign(appID, timestamp, ciphertext string, privateKey *rsa.PrivateKey) (string, error) {
data := appID + timestamp + ciphertext
hash := sha256.Sum256([]byte(data))
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// VerifySign 验证签名
func VerifySign(appID, timestamp, ciphertext, signBase64 string, publicKey *rsa.PublicKey) error {
data := appID + timestamp + ciphertext
hash := sha256.Sum256([]byte(data))
signature, err := base64.StdEncoding.DecodeString(signBase64)
if err != nil {
return err
}
return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hash[:], signature)
}
// ParsePrivateKey 解析 PEM 格式的 RSA 私钥
func ParsePrivateKey(pemStr string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(pemStr))
if block == nil {
return nil, errors.New("failed to parse PEM block containing private key")
}
// 尝试 PKCS1
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err == nil {
return key, nil
}
// 尝试 PKCS8
key8, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err == nil {
if rsaKey, ok := key8.(*rsa.PrivateKey); ok {
return rsaKey, nil
}
return nil, errors.New("parsed key is not RSA")
}
return nil, fmt.Errorf("failed to parse private key: %v", err)
}
// ParsePublicKey 解析 PEM 格式的 RSA 公钥
func ParsePublicKey(pemStr string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pemStr))
if block == nil {
return nil, errors.New("failed to parse PEM block containing public key")
}
// 尝试 PKIX
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err == nil {
if rsaKey, ok := key.(*rsa.PublicKey); ok {
return rsaKey, nil
}
return nil, errors.New("parsed key is not RSA")
}
// 尝试 PKCS1
rsaKey, err := x509.ParsePKCS1PublicKey(block.Bytes)
if err == nil {
return rsaKey, nil
}
return nil, fmt.Errorf("failed to parse public key: %v", err)
}
// RemoveZeroValuesAndSort 将结构体转为 map过滤零值按键排序返回 JSON 字符串
func RemoveZeroValuesAndSort(v interface{}) (string, error) {
// 使用反射获取字段
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return "", errors.New("input must be a struct")
}
typ := val.Type()
m := make(map[string]interface{})
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// 处理 omitempty
name := strings.Split(jsonTag, ",")[0]
fieldVal := val.Field(i)
// 检查零值
if fieldVal.IsZero() {
continue
}
m[name] = fieldVal.Interface()
}
// 按键排序
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
// 构建 JSON
var buf bytes.Buffer
buf.WriteByte('{')
for i, k := range keys {
if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(`"` + k + `":`)
// 简单序列化(仅支持基本类型)
v := m[k]
switch val := v.(type) {
case string:
buf.WriteString(`"` + val + `"`)
case int, int32, int64, uint, uint32, uint64:
fmt.Fprintf(&buf, "%d", val)
case float64:
fmt.Fprintf(&buf, "%v", val)
case bool:
fmt.Fprintf(&buf, "%t", val)
default:
// 使用 json.Marshal 处理复杂类型
b, err := json.Marshal(v)
if err != nil {
return "", err
}
buf.Write(b)
}
}
buf.WriteByte('}')
return buf.String(), nil
}
```
// File: ymt_v3_generate/client.go
```go
package ymt_v3_generate
import (
"bytes"
"context"
"crypto/rsa"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client 蓝色兄弟营销开放 API 客户端
type Client struct {
appID string
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
key []byte // AES 加密密钥
baseURL string
httpClient *http.Client
timeout time.Duration
}
// Option 客户端配置选项
type Option func(*Client)
// NewClient 创建客户端
func NewClient(opts ...Option) *Client {
c := &Client{
baseURL: "https://gateway.dev.cdlsxd.cn", // 默认测试环境
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
for _, opt := range opts {
opt(c)
}
return c
}
// WithAppID 设置应用 ID
func WithAppID(appID string) Option {
return func(c *Client) {
c.appID = appID
}
}
// WithPrivateKey 设置私钥PEM 字符串)
func WithPrivateKey(privateKeyPEM string) Option {
return func(c *Client) {
key, err := ParsePrivateKey(privateKeyPEM)
if err != nil {
panic(fmt.Sprintf("invalid private key: %v", err))
}
c.privateKey = key
}
}
// WithPublicKey 设置公钥PEM 字符串)
func WithPublicKey(publicKeyPEM string) Option {
return func(c *Client) {
key, err := ParsePublicKey(publicKeyPEM)
if err != nil {
panic(fmt.Sprintf("invalid public key: %v", err))
}
c.publicKey = key
}
}
// WithKey 设置 AES 加密密钥base64 字符串)
func WithKey(keyBase64 string) Option {
return func(c *Client) {
key, err := base64.StdEncoding.DecodeString(keyBase64)
if err != nil {
panic(fmt.Sprintf("invalid AES key: %v", err))
}
c.key = key
}
}
// WithBaseURL 设置基础 URL
func WithBaseURL(baseURL string) Option {
return func(c *Client) {
c.baseURL = baseURL
}
}
// WithTimeout 设置超时时间
func WithTimeout(timeout time.Duration) Option {
return func(c *Client) {
c.timeout = timeout
c.httpClient.Timeout = timeout
}
}
// WithHTTPClient 设置自定义 HTTP 客户端
func WithHTTPClient(httpClient *http.Client) Option {
return func(c *Client) {
c.httpClient = httpClient
}
}
// Do 发送 API 请求
func (c *Client) Do(ctx context.Context, method, path string, requestBody interface{}, responseBody interface{}) error {
// 1. 将请求体转为排序后的 JSON 字符串plaintext
plaintext, err := RemoveZeroValuesAndSort(requestBody)
if err != nil {
return fmt.Errorf("failed to serialize request body: %w", err)
}
// 2. 加密得到 ciphertext
ciphertext, err := EncryptPlaintext(plaintext, c.key)
if err != nil {
return fmt.Errorf("failed to encrypt request: %w", err)
}
// 3. 生成时间戳
timestamp := time.Now().Format("2006-01-02 15:04:05")
// 4. 生成签名
sign, err := Sign(c.appID, timestamp, ciphertext, c.privateKey)
if err != nil {
return fmt.Errorf("failed to sign request: %w", err)
}
// 5. 构造 HTTP 请求
reqBody := map[string]string{"ciphertext": ciphertext}
reqBytes, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(reqBytes))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Appid", c.appID)
req.Header.Set("Timestamp", timestamp)
req.Header.Set("Sign", sign)
// 6. 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
// 7. 解析公共响应
var apiResp ApiResponse
if err := json.Unmarshal(respBytes, &apiResp); err != nil {
return fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Code != 200 {
return &APIError{
Code: apiResp.Code,
Message: apiResp.Message,
Reason: apiResp.Reason,
}
}
// 8. 解密 data.ciphertext
decrypted, err := DecryptCiphertext(apiResp.Data.Ciphertext, c.key)
if err != nil {
return fmt.Errorf("failed to decrypt response: %w", err)
}
// 9. 反序列化到业务响应
if err := json.Unmarshal([]byte(decrypted), responseBody); err != nil {
return fmt.Errorf("failed to unmarshal business response: %w", err)
}
return nil
}
```
// File: ymt_v3_generate/api_key.go
```go
package ymt_v3_generate
import "context"
// CreateKeyOrder 获取券码
func (c *Client) CreateKeyOrder(ctx context.Context, req *KeyOrderRequest) (*KeyOrderResponse, error) {
var resp KeyOrderResponse
err := c.Do(ctx, "POST", "/openapi/v1/key/order", req, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
// QueryKey 券码查询
func (c *Client) QueryKey(ctx context.Context, req *KeyQueryRequest) (*KeyQueryResponse, error) {
var resp KeyQueryResponse
err := c.Do(ctx, "POST", "/openapi/v1/key/query", req, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
// DiscardKey 券码作废
func (c *Client) DiscardKey(ctx context.Context, req *KeyDiscardRequest) (*KeyDiscardResponse, error) {
var resp KeyDiscardResponse
err := c.Do(ctx, "POST", "/openapi/v1/key/discard", req, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
// BatchOrder 批量发卡
func (c *Client) BatchOrder(ctx context.Context, req *BatchOrderRequest) (*BatchOrderResponse, error) {
var resp BatchOrderResponse
err := c.Do(ctx, "POST", "/openapi/v1/key/batch_order", req, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
// BatchQuery 批量查询
func (c *Client) BatchQuery(ctx context.Context, req *BatchQueryRequest) (*BatchQueryResponse, error) {
var resp BatchQueryResponse
err := c.Do(ctx, "POST", "/openapi/v1/key/batch_query", req, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
```
// File: ymt_v3_generate/example_test.go
```go
package ymt_v3_generate_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
sdk "ymt_v3_generate"
)
// 生成测试用的 RSA 密钥对和 AES key
func generateTestKeys() (privateKeyPEM, publicKeyPEM string, aesKey []byte, err error) {
// RSA 密钥
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", "", nil, err
}
privBytes := x509.MarshalPKCS1PrivateKey(priv)
privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes})
publicKey := &priv.PublicKey
pubBytes, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return "", "", nil, err
}
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes})
// AES key (16字节)
aesKey = make([]byte, 16)
_, err = rand.Read(aesKey)
if err != nil {
return "", "", nil, err
}
return string(privPEM), string(pubPEM), aesKey, nil
}
// 模拟服务器,返回加密的响应
func mockServer(t *testing.T, aesKey []byte, responseBody interface{}) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 验证请求头
appID := r.Header.Get("Appid")
timestamp := r.Header.Get("Timestamp")
sign := r.Header.Get("Sign")
if appID == "" || timestamp == "" || sign == "" {
http.Error(w, "missing headers", http.StatusBadRequest)
return
}
// 读取请求体
var reqBody map[string]string
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
ciphertext := reqBody["ciphertext"]
if ciphertext == "" {
http.Error(w, "missing ciphertext", http.StatusBadRequest)
return
}
// 这里不验证签名,仅测试流程
// 构造响应
respPlaintext, _ := json.Marshal(responseBody)
// 加密
encrypted, err := sdk.EncryptPlaintext(string(respPlaintext), aesKey)
if err != nil {
http.Error(w, "encrypt error", http.StatusInternalServerError)
return
}
apiResp := map[string]interface{}{
"code": 200,
"message": "成功",
"data": map[string]string{
"ciphertext": encrypted,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(apiResp)
}))
}
func TestCreateKeyOrder(t *testing.T) {
privPEM, pubPEM, aesKey, err := generateTestKeys()
if err != nil {
t.Fatal(err)
}
server := mockServer(t, aesKey, sdk.KeyOrderResponse{
OutBizNo: "order_001",
TradeNo: "7251449503000383488",
Key: "aZKdU9BymzR6qGRzJM",
ValidBeginTime: "2026-06-22 15:30:00",
ValidEndTime: "2026-12-31 23:59:59",
UsableNum: 1,
UsageNum: 0,
Status: 1,
SettlementPrice: 9.9,
Account: "18666666666",
})
defer server.Close()
client := sdk.NewClient(
sdk.WithAppID("test_app"),
sdk.WithPrivateKey(privPEM),
sdk.WithPublicKey(pubPEM),
sdk.WithKey(base64.StdEncoding.EncodeToString(aesKey)),
sdk.WithBaseURL(server.URL),
)
req := &sdk.KeyOrderRequest{
OutBizNo: "order_001",
ActivityNo: "ACT20260622001",
Account: "18666666666",
NotifyURL: "https://notify.example.com/openapi",
}
resp, err := client.CreateKeyOrder(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp.TradeNo != "7251449503000383488" {
t.Errorf("expected trade_no 7251449503000383488, got %s", resp.TradeNo)
}
}
func TestQueryKey(t *testing.T) {
privPEM, pubPEM, aesKey, err := generateTestKeys()
if err != nil {
t.Fatal(err)
}
server := mockServer(t, aesKey, sdk.KeyQueryResponse{
OutBizNo: "order_001",
TradeNo: "7251449503000383488",
Key: "aZKdU9BymzR6qGRzJM",
ValidBeginTime: "2026-06-22 15:30:00",
ValidEndTime: "2026-12-31 23:59:59",
UsableNum: 1,
UsageNum: 0,
Status: 1,
SettlementPrice: 9.9,
Account: "18666666666",
})
defer server.Close()
client := sdk.NewClient(
sdk.WithAppID("test_app"),
sdk.WithPrivateKey(privPEM),
sdk.WithPublicKey(pubPEM),
sdk.WithKey(base64.StdEncoding.EncodeToString(aesKey)),
sdk.WithBaseURL(server.URL),
)
req := &sdk.KeyQueryRequest{
OutBizNo: "order_001",
}
resp, err := client.QueryKey(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp.TradeNo != "7251449503000383488" {
t.Errorf("expected trade_no 7251449503000383488, got %s", resp.TradeNo)
}
}
func TestDiscardKey(t *testing.T) {
privPEM, pubPEM, aesKey, err := generateTestKeys()
if err != nil {
t.Fatal(err)
}
server := mockServer(t, aesKey, sdk.KeyDiscardResponse{
OutBizNo: "order_001",
TradeNo: "7251449503000383488",
Status: 3,
})
defer server.Close()
client := sdk.NewClient(
sdk.WithAppID("test_app"),
sdk.WithPrivateKey(privPEM),
sdk.WithPublicKey(pubPEM),
sdk.WithKey(base64.StdEncoding.EncodeToString(aesKey)),
sdk.WithBaseURL(server.URL),
)
req := &sdk.KeyDiscardRequest{
TradeNo: "7251449503000383488",
}
resp, err := client.DiscardKey(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp.Status != 3 {
t.Errorf("expected status 3, got %d", resp.Status)
}
}
func TestBatchOrder(t *testing.T) {
privPEM, pubPEM, aesKey, err := generateTestKeys()
if err != nil {
t.Fatal(err)
}
server := mockServer(t, aesKey, sdk.BatchOrderResponse{
OutBizNo: "batch_001",
TradeNo: "7251449503000383499",
Status: "processing",
})
defer server.Close()
client := sdk.NewClient(
sdk.WithAppID("test_app"),
sdk.WithPrivateKey(privPEM),
sdk.WithPublicKey(pubPEM),
sdk.WithKey(base64.StdEncoding.EncodeToString(aesKey)),
sdk.WithBaseURL(server.URL),
)
req := &sdk.BatchOrderRequest{
OutBizNo: "batch_001",
ActivityNo: "ACT20260622001",
Number: 100,
}
resp, err := client.BatchOrder(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp.Status != "processing" {
t.Errorf("expected status processing, got %s", resp.Status)
}
}
func TestBatchQuery(t *testing.T) {
privPEM, pubPEM, aesKey, err := generateTestKeys()
if err != nil {
t.Fatal(err)
}
server := mockServer(t, aesKey, sdk.BatchQueryResponse{
OutBizNo: "batch_001",
TradeNo: "7251449503000383499",
Status: "success",
DownloadURL: "https://oss.example.com/openapi_7251449503000383499.zip",
ZipPassword: "123456",
})
defer server.Close()
client := sdk.NewClient(
sdk.WithAppID("test_app"),
sdk.WithPrivateKey(privPEM),
sdk.WithPublicKey(pubPEM),
sdk.WithKey(base64.StdEncoding.EncodeToString(aesKey)),
sdk.WithBaseURL(server.URL),
)
req := &sdk.BatchQueryRequest{
TradeNo: "7251449503000383499",
}
resp, err := client.BatchQuery(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp.Status != "success" {
t.Errorf("expected status success, got %s", resp.Status)
}
if resp.DownloadURL == "" {
t.Error("expected download_url not empty")
}
}
```