添加文件: ymt_v3_2/valid.md

This commit is contained in:
renzhiyuan 2026-07-21 15:19:00 +08:00
parent c425abd9aa
commit 071aaf4a67
1 changed files with 695 additions and 0 deletions

695
ymt_v3_2/valid.md Normal file
View File

@ -0,0 +1,695 @@
// 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"
)
type Client struct {
httpClient *http.Client
baseURL string
appID string
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
aesKey []byte
signType string
}
type Option func(*Client)
func WithBaseURL(url string) Option {
return func(c *Client) {
c.baseURL = url
}
}
func WithTimeout(d time.Duration) Option {
return func(c *Client) {
c.httpClient.Timeout = d
}
}
func WithHTTPClient(client *http.Client) Option {
return func(c *Client) {
c.httpClient = client
}
}
func WithAppID(appID string) Option {
return func(c *Client) {
c.appID = appID
}
}
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
}
}
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
}
}
func WithAESKey(key string) Option {
return func(c *Client) {
c.aesKey = []byte(key)
}
}
func WithSignType(signType string) Option {
return func(c *Client) {
c.signType = signType
}
}
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
}
func (c *Client) Do(ctx context.Context, method, path string, bizReq interface{}, bizResp interface{}) error {
plaintext, err := marshalSortedNoZero(bizReq)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
ciphertext, err := aesEncryptECB(plaintext, c.aesKey)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
signStr := c.appID + timestamp + ciphertext
sign, err := rsaSign([]byte(signStr), c.privateKey)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
reqBody := cipherRequest{Ciphertext: ciphertext}
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal request body: %w", err)
}
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")
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)
}
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,
}
}
if len(commonResp.Data) == 0 {
return nil
}
var cd cipherData
if err := json.Unmarshal(commonResp.Data, &cd); err == nil && cd.Ciphertext != "" {
decrypted, err := aesDecryptECB(cd.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)
}
} else {
if err := json.Unmarshal(commonResp.Data, bizResp); err != nil {
return fmt.Errorf("unmarshal plain data: %w", err)
}
}
return nil
}
type cipherRequest struct {
Ciphertext string `json:"ciphertext"`
}
type commonResponse struct {
Code int32 `json:"code"`
Message string `json:"message"`
Reason string `json:"reason,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
type cipherData struct {
Ciphertext string `json:"ciphertext"`
}
```
// File: ymt_v3_2/types.go
```go
package ymt_v3_2
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"`
}
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"`
}
type QueryKeyRequest struct {
OutBizNo string `json:"out_biz_no,omitempty"`
TradeNo string `json:"trade_no,omitempty"`
}
type QueryKeyResponse = GetKeyResponse
type DiscardKeyRequest struct {
OutBizNo string `json:"out_biz_no,omitempty"`
TradeNo string `json:"trade_no,omitempty"`
}
type DiscardKeyResponse struct {
OutBizNo string `json:"out_biz_no"`
TradeNo string `json:"trade_no"`
Status uint32 `json:"status"`
}
type BatchOrderRequest struct {
OutBizNo string `json:"out_biz_no"`
ActivityNo string `json:"activity_no"`
Number int32 `json:"number"`
NotifyURL string `json:"notify_url,omitempty"`
}
type BatchOrderResponse struct {
OutBizNo string `json:"out_biz_no"`
TradeNo string `json:"trade_no"`
Status string `json:"status"`
}
type BatchQueryRequest struct {
OutBizNo string `json:"out_biz_no,omitempty"`
TradeNo string `json:"trade_no,omitempty"`
}
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"
)
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 {
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
}
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
}
func marshalSortedNoZero(v interface{}) ([]byte, error) {
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)
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 nil, err
}
buf.Write(valBytes)
}
buf.WriteByte('}')
return buf.Bytes(), nil
}
func removeZeroValues(m map[string]interface{}) map[string]interface{} {
res := make(map[string]interface{})
for k, v := range m {
if isZeroValue(v) {
continue
}
if sub, ok := v.(map[string]interface{}); ok {
subFiltered := removeZeroValues(sub)
if len(subFiltered) > 0 {
res[k] = subFiltered
}
} else {
res[k] = v
}
}
return res
}
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
}
}
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))
for i := 0; i < len(plaintext); i += blockSize {
block.Encrypt(ciphertext[i:i+blockSize], plaintext[i:i+blockSize])
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
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
}
func pkcs7Padding(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padtext...)
}
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
}
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
}
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)
}
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)
}
var _ = cipher.NewCBCEncrypter
var _ = strings.ReplaceAll
```
// File: ymt_v3_2/errors.go
```go
package ymt_v3_2
import "fmt"
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)
}
```
// File: ymt_v3_2/api_key.go
```go
package ymt_v3_2
import "context"
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
}
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
}
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
}
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
}
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)
}
```