// 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") } } ```