添加文件: ymt_v3_2/generate.md
This commit is contained in:
parent
cd6feb1ef1
commit
c425abd9aa
|
|
@ -0,0 +1,767 @@
|
|||
// File: ymt_v3_2/go.mod
|
||||
```go
|
||||
module ymt_v3_2
|
||||
|
||||
go 1.21
|
||||
```
|
||||
|
||||
// File: ymt_v3_2/client.go
|
||||
```go
|
||||
package ymt_v3_2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client 是 SDK 的客户端,封装了 HTTP 通信、加密、签名等逻辑。
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
appID string
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
aesKey []byte
|
||||
signType string
|
||||
}
|
||||
|
||||
// Option 是配置 Client 的函数选项。
|
||||
type Option func(*Client)
|
||||
|
||||
// WithBaseURL 设置 API 网关地址。
|
||||
func WithBaseURL(url string) Option {
|
||||
return func(c *Client) {
|
||||
c.baseURL = url
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout 设置 HTTP 请求超时时间。
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(c *Client) {
|
||||
c.httpClient.Timeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithHTTPClient 设置自定义的 HTTP 客户端。
|
||||
func WithHTTPClient(client *http.Client) Option {
|
||||
return func(c *Client) {
|
||||
c.httpClient = client
|
||||
}
|
||||
}
|
||||
|
||||
// WithAppID 设置应用 ID。
|
||||
func WithAppID(appID string) Option {
|
||||
return func(c *Client) {
|
||||
c.appID = appID
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrivateKeyPEM 设置应用私钥(PEM 格式字符串)。
|
||||
func WithPrivateKeyPEM(pemData string) Option {
|
||||
return func(c *Client) {
|
||||
key, err := parsePrivateKey(pemData)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to parse private key: %v", err))
|
||||
}
|
||||
c.privateKey = key
|
||||
}
|
||||
}
|
||||
|
||||
// WithPublicKeyPEM 设置平台公钥(PEM 格式字符串),用于验签。
|
||||
func WithPublicKeyPEM(pemData string) Option {
|
||||
return func(c *Client) {
|
||||
key, err := parsePublicKey(pemData)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to parse public key: %v", err))
|
||||
}
|
||||
c.publicKey = key
|
||||
}
|
||||
}
|
||||
|
||||
// WithAESKey 设置业务参数加解密密钥(AES 密钥,16/24/32 字节)。
|
||||
func WithAESKey(key string) Option {
|
||||
return func(c *Client) {
|
||||
c.aesKey = []byte(key)
|
||||
}
|
||||
}
|
||||
|
||||
// WithSignType 设置签名类型,目前仅支持 "RSA"。
|
||||
func WithSignType(signType string) Option {
|
||||
return func(c *Client) {
|
||||
c.signType = signType
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient 创建一个新的客户端实例。
|
||||
func NewClient(opts ...Option) *Client {
|
||||
c := &Client{
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
baseURL: "https://gateway.dev.cdlsxd.cn",
|
||||
signType: "RSA",
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Do 执行一个 API 请求,自动处理加密、签名、解密。
|
||||
// bizReq 为业务请求结构体,bizResp 为业务响应结构体指针。
|
||||
func (c *Client) Do(ctx context.Context, method, path string, bizReq interface{}, bizResp interface{}) error {
|
||||
// 1. 业务参数过滤零值、排序、序列化
|
||||
plaintext, err := marshalSortedNoZero(bizReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
// 2. AES 加密
|
||||
ciphertext, err := aesEncryptECB(plaintext, c.aesKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: %w", err)
|
||||
}
|
||||
|
||||
// 3. 生成时间戳
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
// 4. 签名
|
||||
signStr := c.appID + timestamp + ciphertext
|
||||
sign, err := rsaSign([]byte(signStr), c.privateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: %w", err)
|
||||
}
|
||||
|
||||
// 5. 构造请求体
|
||||
reqBody := cipherRequest{Ciphertext: ciphertext}
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request body: %w", err)
|
||||
}
|
||||
|
||||
// 6. 创建 HTTP 请求
|
||||
url := c.baseURL + path
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Appid", c.appID)
|
||||
req.Header.Set("Timestamp", timestamp)
|
||||
req.Header.Set("Sign", sign)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 7. 发送请求
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
// 8. 解析公共响应
|
||||
var commonResp commonResponse
|
||||
if err := json.Unmarshal(respBytes, &commonResp); err != nil {
|
||||
return fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if commonResp.Code != 200 {
|
||||
return &APIError{
|
||||
Code: commonResp.Code,
|
||||
Message: commonResp.Message,
|
||||
Reason: commonResp.Reason,
|
||||
}
|
||||
}
|
||||
|
||||
// 9. 解密业务数据
|
||||
if commonResp.Data == nil || commonResp.Data.Ciphertext == "" {
|
||||
// 某些接口可能直接返回 data 中的明文(如查询接口文档示例中 ciphertext 为空)
|
||||
// 此时尝试直接解析整个 data 到 bizResp
|
||||
if commonResp.Data != nil {
|
||||
// 如果 data 中有其他字段,尝试直接反序列化 data
|
||||
dataBytes, _ := json.Marshal(commonResp.Data)
|
||||
if err := json.Unmarshal(dataBytes, bizResp); err != nil {
|
||||
return fmt.Errorf("unmarshal plain data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解密 ciphertext
|
||||
decrypted, err := aesDecryptECB(commonResp.Data.Ciphertext, c.aesKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt response: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(decrypted, bizResp); err != nil {
|
||||
return fmt.Errorf("unmarshal decrypted response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cipherRequest 请求体结构
|
||||
type cipherRequest struct {
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
}
|
||||
|
||||
// commonResponse 公共响应结构
|
||||
type commonResponse struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Data *cipherData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// cipherData 加密数据载体
|
||||
type cipherData struct {
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
}
|
||||
```
|
||||
|
||||
// File: ymt_v3_2/types.go
|
||||
```go
|
||||
package ymt_v3_2
|
||||
|
||||
// GetKeyRequest 获取券码请求参数
|
||||
type GetKeyRequest struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
ActivityNo string `json:"activity_no"`
|
||||
Account string `json:"account,omitempty"`
|
||||
NotifyURL string `json:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
// GetKeyResponse 获取券码响应参数
|
||||
type GetKeyResponse struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Key string `json:"key,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
ValidBeginTime string `json:"valid_begin_time,omitempty"`
|
||||
ValidEndTime string `json:"valid_end_time,omitempty"`
|
||||
UsableNum uint32 `json:"usable_num"`
|
||||
UsageNum uint32 `json:"usage_num"`
|
||||
Status uint32 `json:"status"`
|
||||
SettlementPrice float64 `json:"settlement_price,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
}
|
||||
|
||||
// QueryKeyRequest 券码查询请求参数
|
||||
type QueryKeyRequest struct {
|
||||
OutBizNo string `json:"out_biz_no,omitempty"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
}
|
||||
|
||||
// QueryKeyResponse 券码查询响应参数(与获取券码一致)
|
||||
type QueryKeyResponse = GetKeyResponse
|
||||
|
||||
// DiscardKeyRequest 券码作废请求参数
|
||||
type DiscardKeyRequest struct {
|
||||
OutBizNo string `json:"out_biz_no,omitempty"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
}
|
||||
|
||||
// DiscardKeyResponse 券码作废响应参数
|
||||
type DiscardKeyResponse 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"`
|
||||
}
|
||||
```
|
||||
|
||||
// File: ymt_v3_2/crypto.go
|
||||
```go
|
||||
package ymt_v3_2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parsePrivateKey 解析 PEM 格式的 RSA 私钥。
|
||||
func parsePrivateKey(pemData string) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemData))
|
||||
if block == nil {
|
||||
return nil, errors.New("failed to decode PEM block")
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// 尝试 PKCS1
|
||||
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse private key: %w", err)
|
||||
}
|
||||
}
|
||||
rsaKey, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("not an RSA private key")
|
||||
}
|
||||
return rsaKey, nil
|
||||
}
|
||||
|
||||
// parsePublicKey 解析 PEM 格式的 RSA 公钥。
|
||||
func parsePublicKey(pemData string) (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemData))
|
||||
if block == nil {
|
||||
return nil, errors.New("failed to decode PEM block")
|
||||
}
|
||||
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse public key: %w", err)
|
||||
}
|
||||
rsaKey, ok := key.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errors.New("not an RSA public key")
|
||||
}
|
||||
return rsaKey, nil
|
||||
}
|
||||
|
||||
// marshalSortedNoZero 将结构体转为 map,过滤零值,按 key 排序后序列化为 JSON 字符串。
|
||||
func marshalSortedNoZero(v interface{}) ([]byte, error) {
|
||||
// 先转为 map
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 过滤零值
|
||||
filtered := removeZeroValues(raw)
|
||||
|
||||
// 按 key 排序
|
||||
keys := make([]string, 0, len(filtered))
|
||||
for k := range filtered {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 构建有序的 JSON 字符串
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
keyBytes, _ := json.Marshal(k)
|
||||
buf.Write(keyBytes)
|
||||
buf.WriteByte(':')
|
||||
valBytes, err := json.Marshal(filtered[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(valBytes)
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// removeZeroValues 递归移除 map 中的零值(nil, 空字符串, 0, false, 空切片/数组, 空 map)。
|
||||
func removeZeroValues(m map[string]interface{}) map[string]interface{} {
|
||||
res := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
if isZeroValue(v) {
|
||||
continue
|
||||
}
|
||||
// 递归处理嵌套 map
|
||||
if sub, ok := v.(map[string]interface{}); ok {
|
||||
subFiltered := removeZeroValues(sub)
|
||||
if len(subFiltered) > 0 {
|
||||
res[k] = subFiltered
|
||||
}
|
||||
} else {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// isZeroValue 判断值是否为零值。
|
||||
func isZeroValue(v interface{}) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val == ""
|
||||
case float64:
|
||||
return val == 0
|
||||
case bool:
|
||||
return !val
|
||||
case []interface{}:
|
||||
return len(val) == 0
|
||||
case map[string]interface{}:
|
||||
return len(val) == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// aesEncryptECB 使用 AES ECB 模式加密,PKCS7 填充。
|
||||
func aesEncryptECB(plaintext, key []byte) (string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
plaintext = pkcs7Padding(plaintext, blockSize)
|
||||
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
// ECB 模式:逐块加密
|
||||
for i := 0; i < len(plaintext); i += blockSize {
|
||||
block.Encrypt(ciphertext[i:i+blockSize], plaintext[i:i+blockSize])
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// aesDecryptECB 使用 AES ECB 模式解密,PKCS7 去填充。
|
||||
func aesDecryptECB(ciphertext string, key []byte) ([]byte, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
if len(data)%blockSize != 0 {
|
||||
return nil, errors.New("ciphertext is not a multiple of the block size")
|
||||
}
|
||||
plaintext := make([]byte, len(data))
|
||||
for i := 0; i < len(data); i += blockSize {
|
||||
block.Decrypt(plaintext[i:i+blockSize], data[i:i+blockSize])
|
||||
}
|
||||
plaintext, err = pkcs7Unpadding(plaintext, blockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// 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, blockSize int) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, errors.New("empty data")
|
||||
}
|
||||
padding := int(data[length-1])
|
||||
if padding > blockSize || padding == 0 {
|
||||
return nil, errors.New("invalid padding")
|
||||
}
|
||||
for i := length - padding; i < length; i++ {
|
||||
if data[i] != byte(padding) {
|
||||
return nil, errors.New("invalid padding")
|
||||
}
|
||||
}
|
||||
return data[:length-padding], nil
|
||||
}
|
||||
|
||||
// rsaSign 使用 RSA 私钥对数据进行 SHA256 签名,返回 Base64 编码的签名。
|
||||
func rsaSign(data []byte, privateKey *rsa.PrivateKey) (string, error) {
|
||||
hashed := sha256.Sum256(data)
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// rsaVerify 使用 RSA 公钥验证签名。
|
||||
func rsaVerify(data []byte, sign string, publicKey *rsa.PublicKey) error {
|
||||
signBytes, err := base64.StdEncoding.DecodeString(sign)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashed := sha256.Sum256(data)
|
||||
return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signBytes)
|
||||
}
|
||||
|
||||
// VerifyCallbackSign 用于回调验签:根据回调数据、签名、appID、timestamp 验证签名。
|
||||
// callbackData 为回调 body 中的 data 字段(JSON 对象),需要过滤零值排序后加密再验签。
|
||||
func (c *Client) VerifyCallbackSign(appID, timestamp, sign string, callbackData map[string]interface{}) error {
|
||||
// 过滤零值并排序
|
||||
filtered := removeZeroValues(callbackData)
|
||||
keys := make([]string, 0, len(filtered))
|
||||
for k := range filtered {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
keyBytes, _ := json.Marshal(k)
|
||||
buf.Write(keyBytes)
|
||||
buf.WriteByte(':')
|
||||
valBytes, err := json.Marshal(filtered[k])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.Write(valBytes)
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
plaintext := buf.Bytes()
|
||||
|
||||
// 加密
|
||||
ciphertext, err := aesEncryptECB(plaintext, c.aesKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 拼接签名字符串
|
||||
signStr := appID + timestamp + ciphertext
|
||||
return rsaVerify([]byte(signStr), sign, c.publicKey)
|
||||
}
|
||||
|
||||
// 确保 crypto 包被使用(避免未使用导入的编译错误,实际已被使用)
|
||||
var _ = cipher.NewCBCEncrypter
|
||||
var _ = strings.ReplaceAll
|
||||
```
|
||||
|
||||
// File: ymt_v3_2/errors.go
|
||||
```go
|
||||
package ymt_v3_2
|
||||
|
||||
import "fmt"
|
||||
|
||||
// APIError 表示 API 返回的业务错误。
|
||||
type APIError struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Error 实现 error 接口。
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("API error: code=%d, message=%s, reason=%s", e.Code, e.Message, e.Reason)
|
||||
}
|
||||
```
|
||||
|
||||
// File: ymt_v3_2/api_key.go
|
||||
```go
|
||||
package ymt_v3_2
|
||||
|
||||
import "context"
|
||||
|
||||
// GetKey 获取券码
|
||||
func (c *Client) GetKey(ctx context.Context, req *GetKeyRequest) (*GetKeyResponse, error) {
|
||||
var resp GetKeyResponse
|
||||
err := c.Do(ctx, "POST", "/openapi/v1/key/order", req, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// QueryKey 券码查询
|
||||
func (c *Client) QueryKey(ctx context.Context, req *QueryKeyRequest) (*QueryKeyResponse, error) {
|
||||
var resp QueryKeyResponse
|
||||
err := c.Do(ctx, "POST", "/openapi/v1/key/query", req, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DiscardKey 券码作废
|
||||
func (c *Client) DiscardKey(ctx context.Context, req *DiscardKeyRequest) (*DiscardKeyResponse, error) {
|
||||
var resp DiscardKeyResponse
|
||||
err := c.Do(ctx, "POST", "/openapi/v1/key/discard", req, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// 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_2/example_test.go
|
||||
```go
|
||||
package ymt_v3_2_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ymt_v3_2"
|
||||
)
|
||||
|
||||
// 测试参数,请替换为真实值
|
||||
const (
|
||||
testAppID = "xxx"
|
||||
testPrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||
xxx
|
||||
-----END PRIVATE KEY-----`
|
||||
testPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||
xxx
|
||||
-----END PUBLIC KEY-----`
|
||||
testAESKey = "xxxx"
|
||||
testActivityNo = "xxxx"
|
||||
)
|
||||
|
||||
func newTestClient() *ymt_v3_2.Client {
|
||||
return ymt_v3_2.NewClient(
|
||||
ymt_v3_2.WithBaseURL("https://gateway.dev.cdlsxd.cn"),
|
||||
ymt_v3_2.WithAppID(testAppID),
|
||||
ymt_v3_2.WithPrivateKeyPEM(testPrivateKey),
|
||||
ymt_v3_2.WithPublicKeyPEM(testPublicKey),
|
||||
ymt_v3_2.WithAESKey(testAESKey),
|
||||
ymt_v3_2.WithTimeout(10*time.Second),
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetKey(t *testing.T) {
|
||||
client := newTestClient()
|
||||
ctx := context.Background()
|
||||
req := &ymt_v3_2.GetKeyRequest{
|
||||
OutBizNo: fmt.Sprintf("test_%d", time.Now().UnixNano()),
|
||||
ActivityNo: testActivityNo,
|
||||
Account: "18666666666",
|
||||
NotifyURL: "https://notify.example.com/openapi",
|
||||
}
|
||||
resp, err := client.GetKey(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("GetKey failed: %v", err)
|
||||
}
|
||||
t.Logf("GetKey response: %+v", resp)
|
||||
}
|
||||
|
||||
func TestQueryKey(t *testing.T) {
|
||||
client := newTestClient()
|
||||
ctx := context.Background()
|
||||
req := &ymt_v3_2.QueryKeyRequest{
|
||||
OutBizNo: "order_001", // 替换为真实的外部业务号
|
||||
}
|
||||
resp, err := client.QueryKey(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("QueryKey failed: %v", err)
|
||||
}
|
||||
t.Logf("QueryKey response: %+v", resp)
|
||||
}
|
||||
|
||||
func TestDiscardKey(t *testing.T) {
|
||||
client := newTestClient()
|
||||
ctx := context.Background()
|
||||
req := &ymt_v3_2.DiscardKeyRequest{
|
||||
TradeNo: "7251449503000383488", // 替换为真实的交易号
|
||||
}
|
||||
resp, err := client.DiscardKey(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("DiscardKey failed: %v", err)
|
||||
}
|
||||
t.Logf("DiscardKey response: %+v", resp)
|
||||
}
|
||||
|
||||
func TestBatchOrder(t *testing.T) {
|
||||
client := newTestClient()
|
||||
ctx := context.Background()
|
||||
req := &ymt_v3_2.BatchOrderRequest{
|
||||
OutBizNo: fmt.Sprintf("batch_%d", time.Now().UnixNano()),
|
||||
ActivityNo: testActivityNo,
|
||||
Number: 10,
|
||||
NotifyURL: "https://notify.example.com/openapi",
|
||||
}
|
||||
resp, err := client.BatchOrder(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchOrder failed: %v", err)
|
||||
}
|
||||
t.Logf("BatchOrder response: %+v", resp)
|
||||
}
|
||||
|
||||
func TestBatchQuery(t *testing.T) {
|
||||
client := newTestClient()
|
||||
ctx := context.Background()
|
||||
req := &ymt_v3_2.BatchQueryRequest{
|
||||
OutBizNo: "batch_001", // 替换为真实的外部业务号
|
||||
}
|
||||
resp, err := client.BatchQuery(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchQuery failed: %v", err)
|
||||
}
|
||||
t.Logf("BatchQuery response: %+v", resp)
|
||||
}
|
||||
```
|
||||
Loading…
Reference in New Issue