sdk_generate/internal/test/resp.go

4 lines
31 KiB
Go
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.

package test
const Res = "// File: ymt_sdk_v3/go.mod\n```go\nmodule ymt_sdk_v3\n\ngo 1.21\n\nrequire (\n\tgithub.com/tjfoc/gmsm v1.4.2\n)\n```\n\n// File: ymt_sdk_v3/types.go\n```go\npackage ymt_sdk_v3\n\nimport \"time\"\n\n// Config SDK 配置\ntype Config struct {\n\tAppID string // 应用ID\n\tPrivateKey string // 应用私钥\n\tPublicKey string // 平台公钥\n\tKey string // 业务参数加密key\n\tSignType string // 签名类型,默认 RSA\n\tBaseURL string // API 基础地址\n\tTimeout time.Duration\n}\n\n// ClientOption 客户端配置选项\ntype ClientOption func(*Client)\n\n// CommonRequest 公共请求体\ntype CommonRequest struct {\n\tCiphertext string `json:\"ciphertext\"`\n}\n\n// CommonResponse 公共响应体\ntype CommonResponse struct {\n\tCode int32 `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tReason string `json:\"reason,omitempty\"`\n\tData *RespData `json:\"data,omitempty\"`\n}\n\n// RespData 响应数据\ntype RespData struct {\n\tCiphertext string `json:\"ciphertext,omitempty\"`\n}\n\n// OrderKeyRequest 获取券码业务请求参数\ntype OrderKeyRequest struct {\n\tOutBizNo string `json:\"out_biz_no\"` // 外部业务号,幂等\n\tActivityNo string `json:\"activity_no\"` // 活动编号\n\tAccount string `json:\"account,omitempty\"` // 账号,按活动类型透传\n\tNotifyURL string `json:\"notify_url,omitempty\"` // 回调通知地址\n}\n\n// KeyInfo 券码信息(获取券码、查询券码响应)\ntype KeyInfo struct {\n\tOutBizNo string `json:\"out_biz_no\"` // 外部业务号\n\tTradeNo string `json:\"trade_no\"` // 交易号\n\tKey string `json:\"key,omitempty\"` // 卡密\n\tURL string `json:\"url,omitempty\"` // 链接型活动返回短链接\n\tValidBeginTime string `json:\"valid_begin_time,omitempty\"` // 生效时间\n\tValidEndTime string `json:\"valid_end_time,omitempty\"` // 失效时间\n\tUsableNum uint32 `json:\"usable_num\"` // 可用次数\n\tUsageNum uint32 `json:\"usage_num\"` // 已使用次数\n\tStatus uint32 `json:\"status\"` // 状态1 正常2 已核销3 已作废\n\tSettlementPrice float64 `json:\"settlement_price,omitempty\"` // 结算价\n\tAccount string `json:\"account,omitempty\"` // 上报账号\n}\n\n// QueryKeyRequest 券码查询业务请求参数\ntype QueryKeyRequest struct {\n\tOutBizNo string `json:\"out_biz_no,omitempty\"` // 外部业务号,与 trade_no 二选一\n\tTradeNo string `json:\"trade_no,omitempty\"` // 交易号,与 out_biz_no 二选一\n}\n\n// DiscardKeyRequest 券码作废业务请求参数\ntype DiscardKeyRequest struct {\n\tOutBizNo string `json:\"out_biz_no,omitempty\"` // 外部业务号,与 trade_no 二选一\n\tTradeNo string `json:\"trade_no,omitempty\"` // 交易号,与 out_biz_no 二选一\n}\n\n// DiscardKeyResponse 券码作废业务响应参数\ntype DiscardKeyResponse struct {\n\tOutBizNo string `json:\"out_biz_no\"` // 外部业务号\n\tTradeNo string `json:\"trade_no\"` // 交易号\n\tStatus uint32 `json:\"status\"` // 3 表示已作废\n}\n\n// BatchOrderKeyRequest 批量发卡业务请求参数\ntype BatchOrderKeyRequest struct {\n\tOutBizNo string `json:\"out_biz_no\"` // 外部业务号,幂等\n\tActivityNo string `json:\"activity_no\"` // 活动编号\n\tNumber int32 `json:\"number\"` // 发卡数量\n\tNotifyURL string `json:\"notify_url,omitempty\"` // 回调通知地址\n}\n\n// BatchOrderKeyResponse 批量发卡业务响应参数\ntype BatchOrderKeyResponse struct {\n\tOutBizNo string `json:\"out_biz_no\"` // 外部业务号\n\tTradeNo string `json:\"trade_no\"` // 交易号\n\tStatus string `json:\"status\"` // 任务状态,初始返回 processing\n}\n\n// BatchQueryKeyRequest 批量查询业务请求参数\ntype BatchQueryKeyRequest struct {\n\tOutBizNo string `json:\"out_biz_no,omitempty\"` // 外部业务号,与 trade_no 二选一\n\tTradeNo string `json:\"trade_no,omitempty\"` // 交易号,与 out_biz_no 二选一\n}\n\n// BatchQueryKeyResponse 批量查询业务响应参数\ntype BatchQueryKeyResponse struct {\n\tOutBizNo string `json:\"out_biz_no\"` // 外部业务号\n\tTradeNo string `json:\"trade_no\"` // 交易号\n\tStatus string `json:\"status\"` // processing / success / failed\n\tDownloadURL string `json:\"download_url,omitempty\"` // 批量任务成功后返回下载地址\n\tZipPassword string `json:\"zip_password,omitempty\"` // 批量任务成功后返回压缩包密码\n}\n\n// API 路由常量\nconst (\n\tpathOrderKey = \"/openapi/v1/key/order\"\n\tpathQueryKey = \"/openapi/v1/key/query\"\n\tpathDiscardKey = \"/openapi/v1/key/discard\"\n\tpathBatchOrder = \"/openapi/v1/key/batch_order\"\n\tpathBatchQuery = \"/openapi/v1/key/batch_query\"\n)\n\n// 环境地址常量\nconst (\n\tDevBaseURL = \"https://gateway.dev.cdlsxd.cn\"\n\tProdBaseURL = \"https://market.api.86698.cn\"\n)\n\n// 时间格式\nconst TimeFormat = \"2006-01-02 15:04:05\"\n```\n\n// File: ymt_sdk_v3/crypto.go\n```go\npackage ymt_sdk_v3\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// removeZeroValues 移除 map 中的零值字段\nfunc removeZeroValues(data map[string]interface{}) map[string]interface{} {\n\tresult := make(map[string]interface{})\n\tfor k, v := range data {\n\t\tif isZeroValue(v) {\n\t\t\tcontinue\n\t\t}\n\t\tresult[k] = v\n\t}\n\treturn result\n}\n\n// isZeroValue 判断是否为零值\nfunc isZeroValue(v interface{}) bool {\n\tif v == nil {\n\t\treturn true\n\t}\n\tswitch val := v.(type) {\n\tcase string:\n\t\treturn val == \"\"\n\tcase int:\n\t\treturn val == 0\n\tcase int32:\n\t\treturn val == 0\n\tcase int64:\n\t\treturn val == 0\n\tcase uint:\n\t\treturn val == 0\n\tcase uint32:\n\t\treturn val == 0\n\tcase uint64:\n\t\treturn val == 0\n\tcase float32:\n\t\treturn val == 0\n\tcase float64:\n\t\treturn val == 0\n\tcase bool:\n\t\treturn !val\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// sortedKeys 获取排序后的 key 列表\nfunc sortedKeys(m map[string]interface{}) []string {\n\tkeys := make([]string, 0, len(m))\n\tfor k := range m {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\treturn keys\n}\n\n// marshalSorted 按 key 排序序列化 JSON\nfunc marshalSorted(data interface{}) (string, error) {\n\t// 先序列化为 JSON再反序列化为 map\n\tjsonBytes, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"marshal data failed: %w\", err)\n\t}\n\n\tvar m map[string]interface{}\n\tif err := json.Unmarshal(jsonBytes, &m); err != nil {\n\t\treturn \"\", fmt.Errorf(\"unmarshal to map failed: %w\", err)\n\t}\n\n\t// 移除零值\n\tm = removeZeroValues(m)\n\n\t// 按 key 排序\n\tkeys := sortedKeys(m)\n\n\t// 构建有序的 JSON 字符串\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('{')\n\tfor i, k := range keys {\n\t\tif i > 0 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t\t// 序列化 key\n\t\tkeyBytes, _ := json.Marshal(k)\n\t\tbuf.Write(keyBytes)\n\t\tbuf.WriteByte(':')\n\t\t// 序列化 value\n\t\tvalBytes, err := json.Marshal(m[k])\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"marshal value failed: %w\", err)\n\t\t}\n\t\tbuf.Write(valBytes)\n\t}\n\tbuf.WriteByte('}')\n\n\treturn buf.String(), nil\n}\n\n// pkcs7Pad PKCS7 填充\nfunc pkcs7Pad(data []byte, blockSize int) []byte {\n\tpadding := blockSize - len(data)%blockSize\n\tpadText := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(data, padText...)\n}\n\n// pkcs7Unpad PKCS7 去填充\nfunc pkcs7Unpad(data []byte) ([]byte, error) {\n\tlength := len(data)\n\tif length == 0 {\n\t\treturn nil, errors.New(\"pkcs7: invalid padding size\")\n\t}\n\tpadding := int(data[length-1])\n\tif padding > length {\n\t\treturn nil, errors.New(\"pkcs7: invalid padding size\")\n\t}\n\tfor i := 0; i < padding; i++ {\n\t\tif data[length-1-i] != byte(padding) {\n\t\t\treturn nil, errors.New(\"pkcs7: invalid padding\")\n\t\t}\n\t}\n\treturn data[:length-padding], nil\n}\n\n// AesEncrypt AES-ECB 加密\nfunc AesEncrypt(plaintext, key string) (string, error) {\n\tkeyBytes := []byte(key)\n\tblock, err := aes.NewCipher(keyBytes)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"aes new cipher failed: %w\", err)\n\t}\n\n\t// ECB 模式需要填充\n\tpadded := pkcs7Pad([]byte(plaintext), block.BlockSize())\n\n\t// ECB 加密\n\tciphertext := make([]byte, len(padded))\n\tfor i := 0; i < len(padded); i += block.BlockSize() {\n\t\tblock.Encrypt(ciphertext[i:i+block.BlockSize()], padded[i:i+block.BlockSize()])\n\t}\n\n\treturn base64.StdEncoding.EncodeToString(ciphertext), nil\n}\n\n// AesDecrypt AES-ECB 解密\nfunc AesDecrypt(ciphertext, key string) (string, error) {\n\tkeyBytes := []byte(key)\n\tcipherBytes, err := base64.StdEncoding.DecodeString(ciphertext)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"base64 decode failed: %w\", err)\n\t}\n\n\tblock, err := aes.NewCipher(keyBytes)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"aes new cipher failed: %w\", err)\n\t}\n\n\tif len(cipherBytes)%block.BlockSize() != 0 {\n\t\treturn \"\", errors.New(\"ciphertext is not a multiple of block size\")\n\t}\n\n\t// ECB 解密\n\tplaintext := make([]byte, len(cipherBytes))\n\tfor i := 0; i < len(cipherBytes); i += block.BlockSize() {\n\t\tblock.Decrypt(plaintext[i:i+block.BlockSize()], cipherBytes[i:i+block.BlockSize()])\n\t}\n\n\t// 去填充\n\tunpadded, err := pkcs7Unpad(plaintext)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"pkcs7 unpad failed: %w\", err)\n\t}\n\n\treturn string(unpadded), nil\n}\n\n// parseRSAPrivateKey 解析 RSA 私钥\nfunc parseRSAPrivateKey(privateKeyStr string) (*rsa.PrivateKey, error) {\n\tprivateKeyStr = strings.TrimSpace(privateKeyStr)\n\tif !strings.Contains(privateKeyStr, \"-----BEGIN\") {\n\t\t// 尝试 base64 解码\n\t\tderBytes, err := base64.StdEncoding.DecodeString(privateKeyStr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode private key base64 failed: %w\", err)\n\t\t}\n\t\tpriv, err := x509.ParsePKCS8PrivateKey(derBytes)\n\t\tif err != nil {\n\t\t\tpriv, err = x509.ParsePKCS1PrivateKey(derBytes)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"parse private key failed: %w\", err)\n\t\t\t}\n\t\t}\n\t\trsaPriv, ok := priv.(*rsa.PrivateKey)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"not an RSA private key\")\n\t\t}\n\t\treturn rsaPriv, nil\n\t}\n\n\tblock, _ := pem.Decode([]byte(privateKeyStr))\n\tif block == nil {\n\t\treturn nil, errors.New(\"failed to decode PEM block containing private key\")\n\t}\n\n\tpriv, err := x509.ParsePKCS8PrivateKey(block.Bytes)\n\tif err != nil {\n\t\tpriv, err = x509.ParsePKCS1PrivateKey(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parse private key failed: %w\", err)\n\t\t}\n\t}\n\n\trsaPriv, ok := priv.(*rsa.PrivateKey)\n\tif !ok {\n\t\treturn nil, errors.New(\"not an RSA private key\")\n\t}\n\treturn rsaPriv, nil\n}\n\n// parseRSAPublicKey 解析 RSA 公钥\nfunc parseRSAPublicKey(publicKeyStr string) (*rsa.PublicKey, error) {\n\tpublicKeyStr = strings.TrimSpace(publicKeyStr)\n\tif !strings.Contains(publicKeyStr, \"-----BEGIN\") {\n\t\t// 尝试 base64 解码\n\t\tderBytes, err := base64.StdEncoding.DecodeString(publicKeyStr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode public key base64 failed: %w\", err)\n\t\t}\n\t\tpub, err := x509.ParsePKIXPublicKey(derBytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parse public key failed: %w\", err)\n\t\t}\n\t\trsaPub, ok := pub.(*rsa.PublicKey)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"not an RSA public key\")\n\t\t}\n\t\treturn rsaPub, nil\n\t}\n\n\tblock, _ := pem.Decode([]byte(publicKeyStr))\n\tif block == nil {\n\t\treturn nil, errors.New(\"failed to decode PEM block containing public key\")\n\t}\n\n\tpub, err := x509.ParsePKIXPublicKey(block.Bytes)\n\tif err != nil {\n\t\t// 尝试解析 PKCS1\n\t\tpub, err = x509.ParsePKCS1PublicKey(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parse public key failed: %w\", err)\n\t\t}\n\t}\n\n\trsaPub, ok := pub.(*rsa.PublicKey)\n\tif !ok {\n\t\treturn nil, errors.New(\"not an RSA public key\")\n\t}\n\treturn rsaPub, nil\n}\n\n// RSASign RSA 签名\nfunc RSASign(appID, timestamp, ciphertext, privateKeyStr string) (string, error) {\n\tprivateKey, err := parseRSAPrivateKey(privateKeyStr)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"parse private key failed: %w\", err)\n\t}\n\n\t// 拼接签名字符串: appID + timestamp + ciphertext\n\tsignStr := appID + timestamp + ciphertext\n\n\t// SHA256 签名\n\thashed := sha256.Sum256([]byte(signStr))\n\tsignature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"rsa sign failed: %w\", err)\n\t}\n\n\treturn base64.StdEncoding.EncodeToString(signature), nil\n}\n\n// RSAVerify RSA 验签\nfunc RSAVerify(appID, timestamp, ciphertext, sign, publicKeyStr string) error {\n\tpublicKey, err := parseRSAPublicKey(publicKeyStr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parse public key failed: %w\", err)\n\t}\n\n\tsignBytes, err := base64.StdEncoding.DecodeString(sign)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decode sign base64 failed: %w\", err)\n\t}\n\n\t// 拼接签名字符串\n\tsignStr := appID + timestamp + ciphertext\n\thashed := sha256.Sum256([]byte(signStr))\n\n\terr = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signBytes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"signature verification failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// EncryptBizParams 加密业务参数\nfunc EncryptBizParams(bizParams interface{}, key string) (string, error) {\n\t// 1. 去掉零值,按字母排序,转 JSON 字符串得到 plaintext\n\tplaintext, err := marshalSorted(bizParams)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"marshal biz params failed: %w\", err)\n\t}\n\n\t// 2. 使用 key 加密得到 ciphertext\n\tciphertext, err := AesEncrypt(plaintext, key)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"encrypt biz params failed: %w\", err)\n\t}\n\n\treturn ciphertext, nil\n}\n\n// DecryptBizParams 解密业务参数\nfunc DecryptBizParams(ciphertext, key string, result interface{}) error {\n\tplaintext, err := AesDecrypt(ciphertext, key)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decrypt biz params failed: %w\", err)\n\t}\n\n\tif err := json.Unmarshal([]byte(plaintext), result); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal decrypted data failed: %w\", err)\n\t}\n\n\treturn nil\n}\n```\n\n// File: ymt_sdk_v3/errors.go\n```go\npackage ymt_sdk_v3\n\nimport \"fmt\"\n\n// APIError API 错误\ntype APIError struct {\n\tCode int32 `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tReason string `json:\"reason\"`\n}\n\n// Error 实现 error 接口\nfunc (e *APIError) Error() string {\n\tif e.Reason != \"\" {\n\t\treturn fmt.Sprintf(\"api error: code=%d, message=%s, reason=%s\", e.Code, e.Message, e.Reason)\n\t}\n\treturn fmt.Sprintf(\"api error: code=%d, message=%s\", e.Code, e.Message)\n}\n\n// NewAPIError 创建 API 错误\nfunc NewAPIError(code int32, message, reason string) *APIError {\n\treturn &APIError{\n\t\tCode: code,\n\t\tMessage: message,\n\t\tReason: reason,\n\t}\n}\n\n// 公共错误码\nvar (\n\tErrSystemError = NewAPIError(500, \"系统错误\", \"PANIC\")\n\tErrInvalidPayload = NewAPIError(400, \"请求外壳格式错误\", \"INVALID_PAYLOAD\")\n\tErrMissingParam = NewAPIError(400, \"缺少必要参数\", \"MISSING_PARAM\")\n\tErrInvalidTimestamp = NewAPIError(400, \"时间格式错误\", \"INVALID_TIMESTAMP\")\n\tErrDecryptFailed = NewAPIError(400, \"业务参数解密失败\", \"DECRYPT_FAILED\")\n\tErrAppNotFound = NewAPIError(401, \"应用不存在\", \"APP_NOT_FOUND\")\n\tErrInvalidSignature = NewAPIError(401, \"签名错误\", \"INVALID_SIGNATURE\")\n\tErrExpiredTimestamp = NewAPIError(401, \"请求已过期\", \"EXPIRED_TIMESTAMP\")\n\tErrDuplicateRequest = NewAPIError(429, \"重复请求\", \"DUPLICATE_REQUEST\")\n)\n\n// 业务错误码\nvar (\n\tErrActivityNotAuth = NewAPIError(401, \"活动未授权\", \"ACTIVITY_NOT_AUTH\")\n\tErrMerchantNotExist = NewAPIError(401, \"客户不存在\", \"MERCHANT_NOT_EXIST\")\n\tErrMerchantNotAuth = NewAPIError(401, \"客户冻结\", \"MERCHANT_NOT_AUTH\")\n\tErrMerchantAppIncomplete = NewAPIError(401, \"客户应用配置未完善\", \"MERCHANT_APP_INCOMPLETE\")\n\tErrMerchantAppNotAuth = NewAPIError(401, \"应用不存在或未授权\", \"MERCHANT_APP_NOT_AUTH\")\n\tErrActivityExpire = NewAPIError(403, \"活动已结束\", \"ACTIVITY_EXPIRE\")\n\tErrActivityOutOfStock = NewAPIError(403, \"活动剩余量不足\", \"ACTIVITY_OUT_OF_STOCK\")\n\tErrActivityNotExist = NewAPIError(404, \"活动不存在\", \"ACTIVITY_NOT_EXIST\")\n\tErrMerchantOrderNotExist = NewAPIError(404, \"订单不存在\", \"MERCHANT_ORDER_NOT_EXIST\")\n\tErrKeyNotExist = NewAPIError(404, \"key码不存在\", \"KEY_NOT_EXIST\")\n\tErrParamFail = NewAPIError(400, \"参数错误\", \"PARAM_FAIL\")\n\tErrParamDecryptFail = NewAPIError(400, \"明文参数格式错误\", \"PARAM_DECRYPT_FAIL\")\n)\n```\n\n// File: ymt_sdk_v3/client.go\n```go\npackage ymt_sdk_v3\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\n// Client SDK 客户端\ntype Client struct {\n\tappID string\n\tprivateKey string\n\tpublicKey string\n\tkey string\n\tsignType string\n\tbaseURL string\n\thttpClient *http.Client\n}\n\n// NewClient 创建新的 SDK 客户端\nfunc NewClient(appID, privateKey, publicKey, key string, opts ...ClientOption) *Client {\n\tc := &Client{\n\t\tappID: appID,\n\t\tprivateKey: privateKey,\n\t\tpublicKey: publicKey,\n\t\tkey: key,\n\t\tsignType: \"RSA\",\n\t\tbaseURL: DevBaseURL,\n\t\thttpClient: &http.Client{\n\t\t\tTimeout: 30 * time.Second,\n\t\t},\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\n\treturn c\n}\n\n// WithBaseURL 设置基础 URL\nfunc WithBaseURL(baseURL string) ClientOption {\n\treturn func(c *Client) {\n\t\tc.baseURL = baseURL\n\t}\n}\n\n// WithTimeout 设置超时时间\nfunc WithTimeout(timeout time.Duration) ClientOption {\n\treturn func(c *Client) {\n\t\tc.httpClient.Timeout = timeout\n\t}\n}\n\n// WithHTTPClient 设置自定义 HTTP 客户端\nfunc WithHTTPClient(httpClient *http.Client) ClientOption {\n\treturn func(c *Client) {\n\t\tc.httpClient = httpClient\n\t}\n}\n\n// WithSignType 设置签名类型\nfunc WithSignType(signType string) ClientOption {\n\treturn func(c *Client) {\n\t\tc.signType = signType\n\t}\n}\n\n// doRequest 执行 HTTP 请求\nfunc (c *Client) doRequest(ctx context.Context, path string, bizParams interface{}) (*CommonResponse, error) {\n\t// 1. 加密业务参数\n\tciphertext, err := EncryptBizParams(bizParams, c.key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"encrypt biz params failed: %w\", err)\n\t}\n\n\t// 2. 生成时间戳\n\ttimestamp := time.Now().Format(TimeFormat)\n\n\t// 3. 生成签名\n\tsign, err := RSASign(c.appID, timestamp, ciphertext, c.privateKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sign failed: %w\", err)\n\t}\n\n\t// 4. 构建请求体\n\treqBody := CommonRequest{\n\t\tCiphertext: ciphertext,\n\t}\n\treqBodyBytes, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"marshal request body failed: %w\", err)\n\t}\n\n\t// 5. 构建 HTTP 请求\n\turl := c.baseURL + path\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBodyBytes))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request failed: %w\", err)\n\t}\n\n\t// 6. 设置 Header\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Appid\", c.appID)\n\treq.Header.Set(\"Timestamp\", timestamp)\n\treq.Header.Set(\"Sign\", sign)\n\n\t// 7. 发送请求\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"http request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// 8. 读取响应\n\trespBodyBytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read response body failed: %w\", err)\n\t}\n\n\t// 9. 解析响应\n\tvar commonResp CommonResponse\n\tif err := json.Unmarshal(respBodyBytes, &commonResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshal response failed: %w\", err)\n\t}\n\n\t// 10. 检查响应码\n\tif commonResp.Code != http.StatusOK {\n\t\treturn nil, NewAPIError(commonResp.Code, commonResp.Message, commonResp.Reason)\n\t}\n\n\treturn &commonResp, nil\n}\n\n// OrderKey 获取券码\n// 文档: POST /openapi/v1/key/order\nfunc (c *Client) OrderKey(ctx context.Context, req *OrderKeyRequest) (*KeyInfo, error) {\n\tcommonResp, err := c.doRequest(ctx, pathOrderKey, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif commonResp.Data == nil || commonResp.Data.Ciphertext == \"\" {\n\t\treturn nil, fmt.Errorf(\"response data ciphertext is empty\")\n\t}\n\n\tvar keyInfo KeyInfo\n\tif err := DecryptBizParams(commonResp.Data.Ciphertext, c.key, &keyInfo); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt response failed: %w\", err)\n\t}\n\n\treturn &keyInfo, nil\n}\n\n// QueryKey 券码查询\n// 文档: POST /openapi/v1/key/query\nfunc (c *Client) QueryKey(ctx context.Context, req *QueryKeyRequest) (*KeyInfo, error) {\n\tcommonResp, err := c.doRequest(ctx, pathQueryKey, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif commonResp.Data == nil || commonResp.Data.Ciphertext == \"\" {\n\t\treturn nil, fmt.Errorf(\"response data ciphertext is empty\")\n\t}\n\n\tvar keyInfo KeyInfo\n\tif err := DecryptBizParams(commonResp.Data.Ciphertext, c.key, &keyInfo); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt response failed: %w\", err)\n\t}\n\n\treturn &keyInfo, nil\n}\n\n// DiscardKey 券码作废\n// 文档: POST /openapi/v1/key/discard\nfunc (c *Client) DiscardKey(ctx context.Context, req *DiscardKeyRequest) (*DiscardKeyResponse, error) {\n\tcommonResp, err := c.doRequest(ctx, pathDiscardKey, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif commonResp.Data == nil || commonResp.Data.Ciphertext == \"\" {\n\t\treturn nil, fmt.Errorf(\"response data ciphertext is empty\")\n\t}\n\n\tvar discardResp DiscardKeyResponse\n\tif err := DecryptBizParams(commonResp.Data.Ciphertext, c.key, &discardResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt response failed: %w\", err)\n\t}\n\n\treturn &discardResp, nil\n}\n\n// BatchOrderKey 批量发卡\n// 文档: POST /openapi/v1/key/batch_order\nfunc (c *Client) BatchOrderKey(ctx context.Context, req *BatchOrderKeyRequest) (*BatchOrderKeyResponse, error) {\n\tcommonResp, err := c.doRequest(ctx, pathBatchOrder, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif commonResp.Data == nil || commonResp.Data.Ciphertext == \"\" {\n\t\treturn nil, fmt.Errorf(\"response data ciphertext is empty\")\n\t}\n\n\tvar batchResp BatchOrderKeyResponse\n\tif err := DecryptBizParams(commonResp.Data.Ciphertext, c.key, &batchResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt response failed: %w\", err)\n\t}\n\n\treturn &batchResp, nil\n}\n\n// BatchQueryKey 批量查询\n// 文档: POST /openapi/v1/key/batch_query\nfunc (c *Client) BatchQueryKey(ctx context.Context, req *BatchQueryKeyRequest) (*BatchQueryKeyResponse, error) {\n\tcommonResp, err := c.doRequest(ctx, pathBatchQuery, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif commonResp.Data == nil || commonResp.Data.Ciphertext == \"\" {\n\t\treturn nil, fmt.Errorf(\"response data ciphertext is empty\")\n\t}\n\n\tvar batchResp BatchQueryKeyResponse\n\tif err := DecryptBizParams(commonResp.Data.Ciphertext, c.key, &batchResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt response failed: %w\", err)\n\t}\n\n\treturn &batchResp, nil\n}\n```\n\n// File: ymt_sdk_v3/example_test.go\n```go\npackage ymt_sdk_v3_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\tymt \"ymt_sdk_v3\"\n)\n\n// 测试配置 - 请替换为实际测试参数\nvar (\n\ttestAppID = \"xxx\"\n\ttestPrivateKey = \"xxx\"\n\ttestPublicKey = \"xxx\"\n\ttestKey = \"xxxx\"\n\ttestActivityNo = \"xxxx\"\n)\n\n// newTestClient 创建测试客户端\nfunc newTestClient() *ymt.Client {\n\treturn ymt.NewClient(\n\t\ttestAppID,\n\t\ttestPrivateKey,\n\t\ttestPublicKey,\n\t\ttestKey,\n\t\tymt.WithBaseURL(ymt.DevBaseURL),\n\t\tymt.WithTimeout(30*time.Second),\n\t)\n}\n\n// TestNewClient 测试客户端创建\nfunc TestNewClient(t *testing.T) {\n\tclient := newTestClient()\n\tif client == nil {\n\t\tt.Fatal(\"client should not be nil\")\n\t}\n\tt.Log(\"client created successfully\")\n}\n\n// TestOrderKey 测试获取券码接口\nfunc TestOrderKey(t *testing.T) {\n\tclient := newTestClient()\n\tctx := context.Background()\n\n\treq := &ymt.OrderKeyRequest{\n\t\tOutBizNo: fmt.Sprintf(\"test_order_%d\", time.Now().Unix()),\n\t\tActivityNo: testActivityNo,\n\t\tAccount: \"18666666666\",\n\t\tNotifyURL: \"https://notify.example.com/openapi\",\n\t}\n\n\tresp, err := client.OrderKey(ctx, req)\n\tif err != nil {\n\t\tt.Logf(\"OrderKey expected error (using test credentials): %v\", err)\n\t\treturn\n\t}\n\n\tt.Logf(\"OrderKey success: out_biz_no=%s, trade_no=%s, key=%s, status=%d\",\n\t\tresp.OutBizNo, resp.TradeNo, resp.Key, resp.Status)\n}\n\n// TestQueryKey 测试券码查询接口\nfunc TestQueryKey(t *testing.T) {\n\tclient := newTestClient()\n\tctx := context.Background()\n\n\treq := &ymt.QueryKeyRequest{\n\t\tOutBizNo: fmt.Sprintf(\"test_order_%d\", time.Now().Unix()),\n\t}\n\n\tresp, err := client.QueryKey(ctx, req)\n\tif err != nil {\n\t\tt.Logf(\"QueryKey expected error (using test credentials): %v\", err)\n\t\treturn\n\t}\n\n\tt.Logf(\"QueryKey success: out_biz_no=%s, trade_no=%s, key=%s, status=%d\",\n\t\tresp.OutBizNo, resp.TradeNo, resp.Key, resp.Status)\n}\n\n// TestDiscardKey 测试券码作废接口\nfunc TestDiscardKey(t *testing.T) {\n\tclient := newTestClient()\n\tctx := context.Background()\n\n\treq := &ymt.DiscardKeyRequest{\n\t\tOutBizNo: fmt.Sprintf(\"test_order_%d\", time.Now().Unix()),\n\t}\n\n\tresp, err := client.DiscardKey(ctx, req)\n\tif err != nil {\n\t\tt.Logf(\"DiscardKey expected error (using test credentials): %v\", err)\n\t\treturn\n\t}\n\n\tt.Logf(\"DiscardKey success: out_biz_no=%s, trade_no=%s, status=%d\",\n\t\tresp.OutBizNo, resp.TradeNo, resp.Status)\n}\n\n// TestBatchOrderKey 测试批量发卡接口\nfunc TestBatchOrderKey(t *testing.T) {\n\tclient := newTestClient()\n\tctx := context.Background()\n\n\treq := &ymt.BatchOrderKeyRequest{\n\t\tOutBizNo: fmt.Sprintf(\"test_batch_%d\", time.Now().Unix()),\n\t\tActivityNo: testActivityNo,\n\t\tNumber: 10,\n\t\tNotifyURL: \"https://notify.example.com/openapi\",\n\t}\n\n\tresp, err := client.BatchOrderKey(ctx, req)\n\tif err != nil {\n\t\tt.Logf(\"BatchOrderKey expected error (using test credentials): %v\", err)\n\t\treturn\n\t}\n\n\tt.Logf(\"BatchOrderKey success: out_biz_no=%s, trade_no=%s, status=%s\",\n\t\tresp.OutBizNo, resp.TradeNo, resp.Status)\n}\n\n// TestBatchQueryKey 测试批量查询接口\nfunc TestBatchQueryKey(t *testing.T) {\n\tclient := newTestClient()\n\tctx := context.Background()\n\n\treq := &ymt.BatchQueryKeyRequest{\n\t\tOutBizNo: fmt.Sprintf(\"test_batch_%d\", time.Now().Unix()),\n\t}\n\n\tresp, err := client.BatchQueryKey(ctx, req)\n\tif err != nil {\n\t\tt.Logf(\"BatchQueryKey expected error (using test credentials): %v\", err)\n\t\treturn\n\t}\n\n\tt.Logf(\"BatchQueryKey success: out_biz_no=%s, trade_no=%s, status=%s, download_url=%s\",\n\t\tresp.OutBizNo, resp.TradeNo, resp.Status, resp.DownloadURL)\n}\n\n// TestAesEncryptDecrypt 测试 AES 加解密\nfunc TestAesEncryptDecrypt(t *testing.T) {\n\t// AES key 必须是 16/24/32 字节\n\tkey := \"1234567890123456\" // 16 bytes = AES-128\n\tplaintext := `{\"activity_no\":\"ACT20260622001\",\"out_biz_no\":\"order_001\"}`\n\n\tciphertext, err := ymt.AesEncrypt(plaintext, key)\n\tif err != nil {\n\t\tt.Fatalf(\"AesEncrypt failed: %v\", err)\n\t}\n\tt.Logf(\"Encrypted ciphertext: %s\", ciphertext)\n\n\tdecrypted, err := ymt.AesDecrypt(ciphertext, key)\n\tif err != nil {\n\t\tt.Fatalf(\"AesDecrypt failed: %v\", err)\n\t}\n\n\tif decrypted != plaintext {\n\t\tt.Fatalf(\"decrypted text mismatch: expected %s, got %s\", plaintext, decrypted)\n\t}\n\tt.Logf(\"Decrypted plaintext matches: %s\", decrypted)\n}\n\n// ExampleClient_OrderKey 示例: 获取券码\nfunc ExampleClient_OrderKey() {\n\t// 初始化客户端\n\tclient := ymt.NewClient(\n\t\t\"your_app_id\",\n\t\t\"your_private_key\",\n\t\t\"platform_public_key\",\n\t\t\"your_encrypt_key\",\n\t\tymt.WithBaseURL(ymt.ProdBaseURL), // 正式环境\n\t)\n\n\tctx := context.Background()\n\n\t// 构建请求\n\treq := &ymt.OrderKeyRequest{\n\t\tOutBizNo: \"order_001\",\n\t\tActivityNo: \"ACT20260622001\",\n\t\tAccount: \"18666666666\",\n\t}\n\n\t// 调用接口\n\tresp, err := client.OrderKey(ctx, req)\n\tif err != nil {\n\t\tfmt.Printf(\"Error: %v\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Success: trade_no=%s, key=%s\\n\", resp.TradeNo, resp.Key)\n}\n\n// ExampleClient_QueryKey 示例: 查询券码\nfunc ExampleClient_QueryKey() {\n\tclient := ymt.NewClient(\n\t\t\"your_app_id\",\n\t\t\"your_private_key\",\n\t\t\"platform_public_key\",\n\t\t\"your_encrypt_key\",\n\t)\n\n\tctx := context.Background()\n\n\treq := &ymt.QueryKeyRequest{\n\t\tTradeNo: \"7251449503000383488\",\n\t}\n\n\tresp, err := client.QueryKey(ctx, req)\n\tif err != nil {\n\t\tfmt.Printf(\"Error: %v\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Status: %d\\n\", resp.Status)\n}\n\n// ExampleClient_DiscardKey 示例: 作废券码\nfunc ExampleClient_DiscardKey() {\n\tclient := ymt.NewClient(\n\t\t\"your_app_id\",\n\t\t\"your_private_key\",\n\t\t\"platform_public_key\",\n\t\t\"your_encrypt_key\",\n\t)\n\n\tctx := context.Background()\n\n\treq := &ymt.DiscardKeyRequest{\n\t\tOutBizNo: \"order_001\",\n\t}\n\n\tresp, err := client.DiscardKey(ctx, req)\n\tif err != nil {\n\t\tfmt.Printf(\"Error: %v\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Discarded: status=%d\\n\", resp.Status)\n}\n\n// ExampleClient_BatchOrderKey 示例: 批量发卡\nfunc ExampleClient_BatchOrderKey() {\n\tclient := ymt.NewClient(\n\t\t\"your_app_id\",\n\t\t\"your_private_key\",\n\t\t\"platform_public_key\",\n\t\t\"your_encrypt_key\",\n\t)\n\n\tctx := context.Background()\n\n\treq := &ymt.BatchOrderKeyRequest{\n\t\tOutBizNo: \"batch_001\",\n\t\tActivityNo: \"ACT20260622001\",\n\t\tNumber: 100,\n\t}\n\n\tresp, err := client.BatchOrderKey(ctx, req)\n\tif err != nil {\n\t\tfmt.Printf(\"Error: %v\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Batch order submitted: trade_no=%s, status=%s\\n\", resp.TradeNo, resp.Status)\n}\n\n// ExampleClient_BatchQueryKey 示例: 批量查询\nfunc ExampleClient_BatchQueryKey() {\n\tclient := ymt.NewClient(\n\t\t\"your_app_id\",\n\t\t\"your_private_key\",\n\t\t\"platform_public_key\",\n\t\t\"your_encrypt_key\",\n\t)\n\n\tctx := context.Background()\n\n\treq := &ymt.BatchQueryKeyRequest{\n\t\tTradeNo: \"7251449503000383499\",\n\t}\n\n\tresp, err := client.BatchQueryKey(ctx, req)\n\tif err != nil {\n\t\tfmt.Printf(\"Error: %v\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Batch status: %s\\n\", resp.Status)\n\tif resp.Status == \"success\" {\n\t\tfmt.Printf(\"Download URL: %s\\n\", resp.DownloadURL)\n\t\tfmt.Printf(\"Zip password: %s\\n\", resp.ZipPassword)\n\t}\n}\n```"