ymt_v3_2-20260721-151016/ymt_v3_2/generate.md

767 lines
19 KiB
Markdown
Raw Permalink 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_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)
}
```