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