添加文件: ymt_v3_generate/res.md
This commit is contained in:
parent
0b5d96844d
commit
8a8a5bc1ad
|
|
@ -0,0 +1,859 @@
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue