From 2d8ed24f277d8068e0f3936066eda0ccb0cd38f0 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 23 Jul 2026 18:14:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20ymt=5Fv?= =?UTF-8?q?3/generate.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ymt_v3/generate.md | 2092 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2092 insertions(+) create mode 100644 ymt_v3/generate.md diff --git a/ymt_v3/generate.md b/ymt_v3/generate.md new file mode 100644 index 0000000..81ae245 --- /dev/null +++ b/ymt_v3/generate.md @@ -0,0 +1,2092 @@ +。 + +现在我来生成所有 6 个文件。根据文档,签名规则是拼接:app_id + timestamp + ciphertext,然后使用 RSA 私钥签名。业务参数加密使用 AES ECB 或 SM4 CBC。 + +让我生成完整的 SDK 代码。 + +// File: ymt_v3/go.mod +```go +module ymt_v3 + +go 1.21 + +require github.com/tjfoc/gmsm v1.4.1 +``` + +// File: ymt_v3/types.go +```go +package ymt_v3 + +// CommonResponse 公共响应结构 +type CommonResponse struct { + Code int32 `json:"code"` + Message string `json:"message"` + Reason string `json:"reason,omitempty"` + Data *CommonData `json:"data,omitempty"` +} + +// CommonData 公共响应数据(包含加密的ciphertext) +type CommonData struct { + Ciphertext string `json:"ciphertext"` +} + +// 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"` +} + +// 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"` +} + +// NotifyPayload 回调通知的body结构 +type NotifyPayload struct { + Data *CommonData `json:"data"` +} + +// NotifyHeader 回调通知的header +type NotifyHeader struct { + Appid string `json:"Appid"` + Timestamp string `json:"Timestamp"` + Sign string `json:"Sign"` +} +``` + +// File: ymt_v3/errors.go +```go +package ymt_v3 + +import "fmt" + +// APIError 表示API返回的业务错误 +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) +} + +// IsSuccess 判断是否成功 +func (e *APIError) IsSuccess() bool { + return e.Code == 200 +} + +// NewAPIError 创建API错误 +func NewAPIError(code int32, message, reason string) *APIError { + return &APIError{ + Code: code, + Message: message, + Reason: reason, + } +} + +// SDKError SDK内部错误 +type SDKError struct { + Message string + Err error +} + +func (e *SDKError) Error() string { + if e.Err != nil { + return fmt.Sprintf("SDK error: %s: %v", e.Message, e.Err) + } + return fmt.Sprintf("SDK error: %s", e.Message) +} + +func (e *SDKError) Unwrap() error { + return e.Err +} + +// NewSDKError 创建SDK内部错误 +func NewSDKError(msg string, err error) *SDKError { + return &SDKError{Message: msg, Err: err} +} +``` + +// File: ymt_v3/crypto.go +```go +package ymt_v3 + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "reflect" + "sort" + "strings" + "time" + + "github.com/tjfoc/gmsm/sm4" +) + +// EncryptType 加密类型 +type EncryptType string + +const ( + EncryptTypeAES EncryptType = "aes" + EncryptTypeSM4 EncryptType = "sm4" +) + +// ==================== RSA签名 ==================== + +// SignWithRSA 使用RSA私钥对数据进行签名 +// 签名串拼接规则:app_id + timestamp + ciphertext +func SignWithRSA(signStr string, privateKeyPEM string) (string, error) { + block, _ := pem.Decode([]byte(privateKeyPEM)) + if block == nil { + return "", fmt.Errorf("failed to decode PEM private key") + } + + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + // 尝试PKCS1格式 + privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse private key: %v", err) + } + } + + rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey) + if !ok { + return "", fmt.Errorf("not a RSA private key") + } + + h := sha256.New() + h.Write([]byte(signStr)) + hashed := h.Sum(nil) + + signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(signature), nil +} + +// VerifyWithRSA 使用RSA公钥验证签名 +// 验签串拼接规则:app_id + timestamp + ciphertext +func VerifyWithRSA(signStr, signature string, publicKeyPEM string) error { + block, _ := pem.Decode([]byte(publicKeyPEM)) + if block == nil { + return fmt.Errorf("failed to decode PEM public key") + } + + publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse public key: %v", err) + } + + rsaPublicKey, ok := publicKey.(*rsa.PublicKey) + if !ok { + return fmt.Errorf("not a RSA public key") + } + + h := sha256.New() + h.Write([]byte(signStr)) + hashed := h.Sum(nil) + + sigBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return fmt.Errorf("failed to decode signature: %v", err) + } + + return rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed, sigBytes) +} + +// ==================== AES ECB加密 ==================== + +// AESECBEncrypt AES-ECB模式加密 +func AESECBEncrypt(plaintext []byte, key []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建AES cipher失败: %v", err) + } + + padded := pkcs7Padding(plaintext, aes.BlockSize) + + ciphertext := make([]byte, len(padded)) + for i := 0; i < len(padded); i += aes.BlockSize { + block.Encrypt(ciphertext[i:i+aes.BlockSize], padded[i:i+aes.BlockSize]) + } + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// AESECBDecrypt AES-ECB模式解密 +func AESECBDecrypt(encryptedData string, key []byte) ([]byte, error) { + ciphertext, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("解码失败: %v", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("创建AES cipher失败: %v", err) + } + + if len(ciphertext)%aes.BlockSize != 0 { + return nil, fmt.Errorf("密文长度不是块大小的整数倍") + } + + plaintext := make([]byte, len(ciphertext)) + for i := 0; i < len(ciphertext); i += aes.BlockSize { + block.Decrypt(plaintext[i:i+aes.BlockSize], ciphertext[i:i+aes.BlockSize]) + } + + plaintext, err = pkcs7UnPadding(plaintext) + if err != nil { + return nil, fmt.Errorf("去除填充失败: %v", err) + } + + return plaintext, nil +} + +// ==================== SM4 CBC加密 ==================== + +// SM4CBCEncrypt SM4-CBC模式加密 +func SM4CBCEncrypt(plaintext []byte, key []byte) (string, error) { + if len(key) != 16 { + return "", fmt.Errorf("SM4密钥长度必须为16字节") + } + + block, err := sm4.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + padded := pkcs7Padding(plaintext, block.BlockSize()) + + iv := make([]byte, block.BlockSize()) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", fmt.Errorf("生成IV失败: %v", err) + } + + mode := NewCBCEncrypter(block, iv) + ciphertext := make([]byte, len(padded)) + mode.CryptBlocks(ciphertext, padded) + + result := append(iv, ciphertext...) + return base64.StdEncoding.EncodeToString(result), nil +} + +// SM4CBCDecrypt SM4-CBC模式解密 +func SM4CBCDecrypt(encryptedData string, key []byte) ([]byte, error) { + if len(key) != 16 { + return nil, fmt.Errorf("SM4密钥长度必须为16字节") + } + + data, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("解码失败: %v", err) + } + + block, err := sm4.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + blockSize := block.BlockSize() + if len(data) < blockSize { + return nil, fmt.Errorf("数据长度不足") + } + iv := data[:blockSize] + ciphertext := data[blockSize:] + + mode := NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + plaintext, err = pkcs7UnPadding(plaintext) + if err != nil { + return nil, fmt.Errorf("去除填充失败: %v", err) + } + + return plaintext, nil +} + +// ==================== PKCS7填充 ==================== + +// pkcs7Padding PKCS7填充 +func pkcs7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) +} + +// pkcs7UnPadding 去除PKCS7填充 +func pkcs7UnPadding(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, fmt.Errorf("数据为空") + } + padding := int(data[length-1]) + if padding > length || padding == 0 { + return nil, fmt.Errorf("无效的填充") + } + for i := length - padding; i < length; i++ { + if data[i] != byte(padding) { + return nil, fmt.Errorf("无效的填充") + } + } + return data[:length-padding], nil +} + +// ==================== CBC模式实现(用于SM4) ==================== + +// cbc 实现了CBC模式加密/解密 +type cbc struct { + b cipherBlock + blockSize int + iv []byte +} + +type cipherBlock interface { + BlockSize() int + Encrypt(dst, src []byte) + Decrypt(dst, src []byte) +} + +// NewCBCEncrypter 创建CBC加密器 +func NewCBCEncrypter(b cipherBlock, iv []byte) *cbc { + return &cbc{ + b: b, + blockSize: b.BlockSize(), + iv: copyBytes(iv), + } +} + +// NewCBCDecrypter 创建CBC解密器 +func NewCBCDecrypter(b cipherBlock, iv []byte) *cbc { + return &cbc{ + b: b, + blockSize: b.BlockSize(), + iv: copyBytes(iv), + } +} + +func copyBytes(src []byte) []byte { + dst := make([]byte, len(src)) + copy(dst, src) + return dst +} + +// CryptBlocks 加密/解密数据块 +func (x *cbc) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + + iv := x.iv + + for i := 0; i < len(src); i += x.blockSize { + // 对于加密:先 XOR IV,再加密 + // 对于解密:先解密,再 XOR IV + // 这里我们通过判断是加密还是解密来区分 + // 简单起见,我们使用一个标志 + } + + // 重新实现:使用内部类型区分 + panic("use Encrypt or Decrypt specific methods") +} + +// 使用标准库的cipher.BlockMode +// 由于sm4.NewCipher返回的是cipher.Block接口,我们可以直接使用cipher.NewCBCEncrypter +// 但为了兼容性,我们使用标准库 + +// ==================== 业务参数加密/解密 ==================== + +// EncryptBizParams 加密业务参数 +// 1. 将业务参数去掉"零"值的参数,再由小到大按照字母排序,转成json字符串得到plaintext +// 2. 使用应用key将plaintext字符串加密,得到ciphertext +func EncryptBizParams(params interface{}, key []byte, encType EncryptType) (string, error) { + // 去掉零值参数并按字母排序 + plaintext, err := marshalWithoutZero(params) + if err != nil { + return "", fmt.Errorf("序列化业务参数失败: %v", err) + } + + switch encType { + case EncryptTypeAES: + return AESECBEncrypt([]byte(plaintext), key) + case EncryptTypeSM4: + return SM4CBCEncrypt([]byte(plaintext), key) + default: + return "", fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// DecryptBizParams 解密业务参数 +func DecryptBizParams(ciphertext string, key []byte, encType EncryptType) ([]byte, error) { + switch encType { + case EncryptTypeAES: + return AESECBDecrypt(ciphertext, key) + case EncryptTypeSM4: + return SM4CBCDecrypt(ciphertext, key) + default: + return nil, fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// marshalWithoutZero 将结构体序列化为JSON,去掉零值字段并按字母排序 +func marshalWithoutZero(v interface{}) (string, error) { + val := reflect.ValueOf(v) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + // 如果是map,直接排序后转json + if val.Kind() == reflect.Map { + return marshalMapSorted(v) + } + // 否则直接json序列化 + data, err := json.Marshal(v) + return string(data), err + } + + result := make(map[string]interface{}) + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // 获取json tag + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + // 处理omitempty + name := strings.Split(jsonTag, ",")[0] + + // 检查是否是零值 + if isZeroValue(field) { + continue + } + + result[name] = field.Interface() + } + + return marshalMapSorted(result) +} + +// marshalMapSorted 将map按键排序后序列化为JSON +func marshalMapSorted(m map[string]interface{}) (string, error) { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + var buf bytes.Buffer + buf.WriteByte('{') + for i, k := range keys { + if i > 0 { + buf.WriteByte(',') + } + // 写入key + keyBytes, _ := json.Marshal(k) + buf.Write(keyBytes) + buf.WriteByte(':') + // 写入value + valBytes, err := json.Marshal(m[k]) + if err != nil { + return "", err + } + buf.Write(valBytes) + } + buf.WriteByte('}') + + return buf.String(), nil +} + +// isZeroValue 判断reflect.Value是否为零值 +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String: + return v.String() == "" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Slice, reflect.Map, reflect.Ptr: + return v.IsNil() + default: + return false + } +} + +// ==================== 签名相关 ==================== + +// BuildSignStr 构建签名字符串 +// 规则:app_id + timestamp + ciphertext +func BuildSignStr(appID, timestamp, ciphertext string) string { + return appID + timestamp + ciphertext +} + +// GenerateTimestamp 生成时间戳,格式 yyyy-MM-dd HH:mm:ss +func GenerateTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +// ==================== 回调验签 ==================== + +// VerifyNotifySign 验证回调通知签名 +// 1. 获取header头里面的签名信息 +// 2. 获取body里面的业务参数data +// 3. 将业务参数data去掉"零"值的参数,再由小到大按照字母排序,转成json字符串得到plaintext +// 4. 使用应用key将plaintext字符串加密得到ciphertext +// 5. 拼接签名字符串:app_id + timestamp + ciphertext +// 6. 使用应用公钥验签 +func VerifyNotifySign(header *NotifyHeader, data interface{}, key []byte, publicKeyPEM string, encType EncryptType) error { + // 3. 将业务参数data去掉零值参数,排序,转json + plaintext, err := marshalWithoutZero(data) + if err != nil { + return fmt.Errorf("序列化回调数据失败: %v", err) + } + + // 4. 加密得到ciphertext + ciphertext, err := EncryptBizParamsRaw([]byte(plaintext), key, encType) + if err != nil { + return fmt.Errorf("加密回调数据失败: %v", err) + } + + // 5. 拼接签名字符串 + signStr := BuildSignStr(header.Appid, header.Timestamp, ciphertext) + + // 6. 使用应用公钥验签 + return VerifyWithRSA(signStr, header.Sign, publicKeyPEM) +} + +// EncryptBizParamsRaw 直接加密字节数据 +func EncryptBizParamsRaw(plaintext []byte, key []byte, encType EncryptType) (string, error) { + switch encType { + case EncryptTypeAES: + return AESECBEncrypt(plaintext, key) + case EncryptTypeSM4: + return SM4CBCEncrypt(plaintext, key) + default: + return "", fmt.Errorf("不支持的加密类型: %s", encType) + } +} +``` + +// File: ymt_v3/client.go +```go +package ymt_v3 + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// Env 环境 +type Env string + +const ( + EnvTest Env = "test" + EnvProduct Env = "product" +) + +// 环境地址 +const ( + testBaseURL = "https://gateway.dev.cdlsxd.cn" + productBaseURL = "https://market.api.86698.cn" +) + +// ClientConfig 客户端配置 +type ClientConfig struct { + AppID string // 应用ID + PrivateKey string // 应用私钥(PEM格式) + PublicKey string // 平台公钥(PEM格式) + Key string // 业务参数加密key + EncryptType EncryptType // 加密类型:aes 或 sm4 + Env Env // 环境:test 或 product + HTTPClient *http.Client // HTTP客户端,可选 +} + +// Client SDK客户端 +type Client struct { + config *ClientConfig + httpClient *http.Client + baseURL string +} + +// NewClient 创建新的SDK客户端 +func NewClient(config *ClientConfig) (*Client, error) { + if config.AppID == "" { + return nil, fmt.Errorf("AppID不能为空") + } + if config.PrivateKey == "" { + return nil, fmt.Errorf("PrivateKey不能为空") + } + if config.PublicKey == "" { + return nil, fmt.Errorf("PublicKey不能为空") + } + if config.Key == "" { + return nil, fmt.Errorf("Key不能为空") + } + if config.EncryptType == "" { + config.EncryptType = EncryptTypeAES + } + if config.Env == "" { + config.Env = EnvTest + } + + var baseURL string + switch config.Env { + case EnvTest: + baseURL = testBaseURL + case EnvProduct: + baseURL = productBaseURL + default: + return nil, fmt.Errorf("不支持的环境: %s", config.Env) + } + + httpClient := config.HTTPClient + if httpClient == nil { + httpClient = &http.Client{} + } + + return &Client{ + config: config, + httpClient: httpClient, + baseURL: baseURL, + }, nil +} + +// doRequest 发送请求并处理响应 +func (c *Client) doRequest(path string, bizParams interface{}, result interface{}) error { + // 1. 加密业务参数 + ciphertext, err := EncryptBizParams(bizParams, []byte(c.config.Key), c.config.EncryptType) + if err != nil { + return NewSDKError("加密业务参数失败", err) + } + + // 2. 生成时间戳 + timestamp := GenerateTimestamp() + + // 3. 构建签名字符串并签名 + signStr := BuildSignStr(c.config.AppID, timestamp, ciphertext) + sign, err := SignWithRSA(signStr, c.config.PrivateKey) + if err != nil { + return NewSDKError("签名失败", err) + } + + // 4. 构建请求体 + reqBody := map[string]string{ + "ciphertext": ciphertext, + } + reqBodyBytes, err := json.Marshal(reqBody) + if err != nil { + return NewSDKError("序列化请求体失败", err) + } + + // 5. 创建HTTP请求 + url := c.baseURL + path + req, err := http.NewRequest("POST", url, bytes.NewReader(reqBodyBytes)) + if err != nil { + return NewSDKError("创建HTTP请求失败", err) + } + + // 6. 设置Header + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Appid", c.config.AppID) + req.Header.Set("Timestamp", timestamp) + req.Header.Set("Sign", sign) + + // 7. 发送请求 + resp, err := c.httpClient.Do(req) + if err != nil { + return NewSDKError("发送HTTP请求失败", err) + } + defer resp.Body.Close() + + // 8. 读取响应 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return NewSDKError("读取响应失败", err) + } + + // 9. 解析公共响应 + var commonResp CommonResponse + if err := json.Unmarshal(respBody, &commonResp); err != nil { + return NewSDKError("解析响应JSON失败", err) + } + + // 10. 检查业务错误 + if commonResp.Code != 200 { + return NewAPIError(commonResp.Code, commonResp.Message, commonResp.Reason) + } + + // 11. 检查是否有加密数据 + if commonResp.Data == nil || commonResp.Data.Ciphertext == "" { + return NewSDKError("响应中没有加密数据", nil) + } + + // 12. 解密响应数据 + decrypted, err := DecryptBizParams(commonResp.Data.Ciphertext, []byte(c.config.Key), c.config.EncryptType) + if err != nil { + return NewSDKError("解密响应数据失败", err) + } + + // 13. 解析解密后的数据到结果 + if err := json.Unmarshal(decrypted, result); err != nil { + return NewSDKError("解析解密后的响应数据失败", err) + } + + return nil +} + +// ==================== API方法 ==================== + +// KeyOrder 获取券码 +// POST /openapi/v1/key/order +func (c *Client) KeyOrder(req *KeyOrderRequest) (*KeyOrderResponse, error) { + result := &KeyOrderResponse{} + if err := c.doRequest("/openapi/v1/key/order", req, result); err != nil { + return nil, err + } + return result, nil +} + +// KeyQuery 券码查询 +// POST /openapi/v1/key/query +func (c *Client) KeyQuery(req *KeyQueryRequest) (*KeyOrderResponse, error) { + result := &KeyOrderResponse{} + if err := c.doRequest("/openapi/v1/key/query", req, result); err != nil { + return nil, err + } + return result, nil +} + +// KeyDiscard 券码作废 +// POST /openapi/v1/key/discard +func (c *Client) KeyDiscard(req *KeyDiscardRequest) (*KeyDiscardResponse, error) { + result := &KeyDiscardResponse{} + if err := c.doRequest("/openapi/v1/key/discard", req, result); err != nil { + return nil, err + } + return result, nil +} + +// BatchOrder 批量发卡 +// POST /openapi/v1/key/batch_order +func (c *Client) BatchOrder(req *BatchOrderRequest) (*BatchOrderResponse, error) { + result := &BatchOrderResponse{} + if err := c.doRequest("/openapi/v1/key/batch_order", req, result); err != nil { + return nil, err + } + return result, nil +} + +// BatchQuery 批量查询 +// POST /openapi/v1/key/batch_query +func (c *Client) BatchQuery(req *BatchQueryRequest) (*BatchQueryResponse, error) { + result := &BatchQueryResponse{} + if err := c.doRequest("/openapi/v1/key/batch_query", req, result); err != nil { + return nil, err + } + return result, nil +} + +// VerifyNotify 验证回调通知 +func (c *Client) VerifyNotify(header *NotifyHeader, data interface{}) error { + return VerifyNotifySign(header, data, []byte(c.config.Key), c.config.PublicKey, c.config.EncryptType) +} + +// GetBaseURL 获取当前环境的基础URL +func (c *Client) GetBaseURL() string { + return c.baseURL +} + +// GetAppID 获取应用ID +func (c *Client) GetAppID() string { + return c.config.AppID +} + +// String 返回客户端配置的字符串表示(隐藏敏感信息) +func (c *Client) String() string { + return fmt.Sprintf("Client{AppID: %s, Env: %s, BaseURL: %s, EncryptType: %s}", + c.config.AppID, c.config.Env, c.baseURL, c.config.EncryptType) +} +``` + +// File: ymt_v3/example_test.go +```go +package ymt_v3 + +import ( + "encoding/json" + "fmt" + "testing" +) + +// 测试参数(请替换为实际值) +const ( + testAppID = "your_app_id" + testPrivateKey = `-----BEGIN PRIVATE KEY----- +your_private_key_here +-----END PRIVATE KEY-----` + testPublicKey = `-----BEGIN PUBLIC KEY----- +your_public_key_here +-----END PUBLIC KEY-----` + testKey = "your_encryption_key_16" + testActivityNo = "your_activity_no" +) + +func ExampleClient_KeyOrder() { + // 创建客户端 + client, err := NewClient(&ClientConfig{ + AppID: testAppID, + PrivateKey: testPrivateKey, + PublicKey: testPublicKey, + Key: testKey, + EncryptType: EncryptTypeAES, + Env: EnvTest, + }) + if err != nil { + fmt.Printf("创建客户端失败: %v\n", err) + return + } + + // 获取券码 + resp, err := client.KeyOrder(&KeyOrderRequest{ + OutBizNo: "order_001", + ActivityNo: testActivityNo, + Account: "18666666666", + NotifyURL: "https://notify.example.com/openapi", + }) + if err != nil { + fmt.Printf("获取券码失败: %v\n", err) + return + } + + fmt.Printf("交易号: %s\n", resp.TradeNo) + fmt.Printf("卡密: %s\n", resp.Key) + fmt.Printf("状态: %d\n", resp.Status) + + // Output: + // 交易号: 7251449503000383488 + // 卡密: aZKdU9BymzR6qGRzJM + // 状态: 1 +} + +func ExampleClient_KeyQuery() { + client, _ := NewClient(&ClientConfig{ + AppID: testAppID, + PrivateKey: testPrivateKey, + PublicKey: testPublicKey, + Key: testKey, + EncryptType: EncryptTypeAES, + Env: EnvTest, + }) + + // 按外部业务号查询 + resp, err := client.KeyQuery(&KeyQueryRequest{ + OutBizNo: "order_001", + }) + if err != nil { + fmt.Printf("查询失败: %v\n", err) + return + } + + fmt.Printf("交易号: %s\n", resp.TradeNo) + fmt.Printf("状态: %d\n", resp.Status) +} + +func ExampleClient_KeyDiscard() { + client, _ := NewClient(&ClientConfig{ + AppID: testAppID, + PrivateKey: testPrivateKey, + PublicKey: testPublicKey, + Key: testKey, + EncryptType: EncryptTypeAES, + Env: EnvTest, + }) + + // 按交易号作废 + resp, err := client.KeyDiscard(&KeyDiscardRequest{ + TradeNo: "7251449503000383488", + }) + if err != nil { + fmt.Printf("作废失败: %v\n", err) + return + } + + fmt.Printf("交易号: %s\n", resp.TradeNo) + fmt.Printf("状态: %d\n", resp.Status) +} + +func ExampleClient_BatchOrder() { + client, _ := NewClient(&ClientConfig{ + AppID: testAppID, + PrivateKey: testPrivateKey, + PublicKey: testPublicKey, + Key: testKey, + EncryptType: EncryptTypeAES, + Env: EnvTest, + }) + + // 批量发卡 + resp, err := client.BatchOrder(&BatchOrderRequest{ + OutBizNo: "batch_001", + ActivityNo: testActivityNo, + Number: 100, + NotifyURL: "https://notify.example.com/openapi", + }) + if err != nil { + fmt.Printf("批量发卡失败: %v\n", err) + return + } + + fmt.Printf("交易号: %s\n", resp.TradeNo) + fmt.Printf("状态: %s\n", resp.Status) +} + +func ExampleClient_BatchQuery() { + client, _ := NewClient(&ClientConfig{ + AppID: testAppID, + PrivateKey: testPrivateKey, + PublicKey: testPublicKey, + Key: testKey, + EncryptType: EncryptTypeAES, + Env: EnvTest, + }) + + // 查询批量任务 + resp, err := client.BatchQuery(&BatchQueryRequest{ + TradeNo: "7251449503000383499", + }) + if err != nil { + fmt.Printf("查询失败: %v\n", err) + return + } + + fmt.Printf("状态: %s\n", resp.Status) + if resp.DownloadURL != "" { + fmt.Printf("下载地址: %s\n", resp.DownloadURL) + } +} + +func TestMarshalWithoutZero(t *testing.T) { + req := KeyOrderRequest{ + OutBizNo: "order_001", + ActivityNo: "ACT20260622001", + // Account和NotifyURL为空,应该被去掉 + } + + result, err := marshalWithoutZero(req) + if err != nil { + t.Fatalf("marshalWithoutZero失败: %v", err) + } + + expected := `{"activity_no":"ACT20260622001","out_biz_no":"order_001"}` + if result != expected { + t.Errorf("期望 %s, 得到 %s", expected, result) + } +} + +func TestBuildSignStr(t *testing.T) { + appID := "123456" + timestamp := "2026-06-22 15:30:00" + ciphertext := "encrypted_data" + + signStr := BuildSignStr(appID, timestamp, ciphertext) + expected := "1234562026-06-22 15:30:00encrypted_data" + if signStr != expected { + t.Errorf("期望 %s, 得到 %s", expected, signStr) + } +} + +func TestGenerateTimestamp(t *testing.T) { + ts := GenerateTimestamp() + // 验证格式 yyyy-MM-dd HH:mm:ss + if len(ts) != 19 { + t.Errorf("时间戳格式错误: %s", ts) + } +} + +func TestEncryptDecryptAES(t *testing.T) { + key := []byte("1234567890123456") // 16字节 + plaintext := `{"out_biz_no":"order_001","activity_no":"ACT001"}` + + ciphertext, err := AESECBEncrypt([]byte(plaintext), key) + if err != nil { + t.Fatalf("加密失败: %v", err) + } + + decrypted, err := AESECBDecrypt(ciphertext, key) + if err != nil { + t.Fatalf("解密失败: %v", err) + } + + if string(decrypted) != plaintext { + t.Errorf("解密结果不匹配: 期望 %s, 得到 %s", plaintext, string(decrypted)) + } +} + +func TestEncryptDecryptSM4(t *testing.T) { + key := []byte("1234567890123456") // 16字节 + plaintext := `{"out_biz_no":"order_001","activity_no":"ACT001"}` + + ciphertext, err := SM4CBCEncrypt([]byte(plaintext), key) + if err != nil { + t.Fatalf("SM4加密失败: %v", err) + } + + decrypted, err := SM4CBCDecrypt(ciphertext, key) + if err != nil { + t.Fatalf("SM4解密失败: %v", err) + } + + if string(decrypted) != plaintext { + t.Errorf("SM4解密结果不匹配: 期望 %s, 得到 %s", plaintext, string(decrypted)) + } +} + +func TestEncryptBizParams(t *testing.T) { + key := []byte("1234567890123456") + req := KeyOrderRequest{ + OutBizNo: "order_001", + ActivityNo: "ACT20260622001", + } + + ciphertext, err := EncryptBizParams(req, key, EncryptTypeAES) + if err != nil { + t.Fatalf("加密业务参数失败: %v", err) + } + + if ciphertext == "" { + t.Fatal("加密结果为空") + } + + // 验证可以解密 + decrypted, err := DecryptBizParams(ciphertext, key, EncryptTypeAES) + if err != nil { + t.Fatalf("解密失败: %v", err) + } + + var result KeyOrderRequest + if err := json.Unmarshal(decrypted, &result); err != nil { + t.Fatalf("解析失败: %v", err) + } + + if result.OutBizNo != "order_001" { + t.Errorf("OutBizNo不匹配: 期望 order_001, 得到 %s", result.OutBizNo) + } +} + +func TestNewClient(t *testing.T) { + _, err := NewClient(&ClientConfig{ + AppID: testAppID, + PrivateKey: testPrivateKey, + PublicKey: testPublicKey, + Key: testKey, + EncryptType: EncryptTypeAES, + Env: EnvTest, + }) + if err != nil { + t.Fatalf("创建客户端失败: %v", err) + } +} + +func TestNewClientMissingFields(t *testing.T) { + tests := []struct { + name string + config *ClientConfig + wantErr bool + }{ + {"空AppID", &ClientConfig{PrivateKey: "pk", PublicKey: "pub", Key: "key"}, true}, + {"空PrivateKey", &ClientConfig{AppID: "app", PublicKey: "pub", Key: "key"}, true}, + {"空PublicKey", &ClientConfig{AppID: "app", PrivateKey: "pk", Key: "key"}, true}, + {"空Key", &ClientConfig{AppID: "app", PrivateKey: "pk", PublicKey: "pub"}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewClient(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("NewClient() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestAPIError(t *testing.T) { + err := NewAPIError(401, "Signature verification failed", "INVALID_SIGNATURE") + if err.Code != 401 { + t.Errorf("Code不匹配: 期望 401, 得到 %d", err.Code) + } + if err.Message != "Signature verification failed" { + t.Errorf("Message不匹配") + } + if err.Reason != "INVALID_SIGNATURE" { + t.Errorf("Reason不匹配") + } + if err.Error() != "API error: code=401, message=Signature verification failed, reason=INVALID_SIGNATURE" { + t.Errorf("Error()不匹配: %s", err.Error()) + } + if err.IsSuccess() { + t.Error("401应该不是成功") + } +} + +func TestSDKError(t *testing.T) { + err := NewSDKError("测试错误", nil) + if err.Error() != "SDK error: 测试错误" { + t.Errorf("Error()不匹配: %s", err.Error()) + } +} +``` + +现在让我检查一下 crypto.go 中的 CBC 实现,我需要修复它,因为之前的实现有 panic。让我重新生成 crypto.go 文件,使用标准库的 cipher.BlockMode 接口。 + +实际上,SM4 的 `sm4.NewCipher` 返回的是 `cipher.Block` 接口,所以我们可以直接使用 `cipher.NewCBCEncrypter` 和 `cipher.NewCBCDecrypter`。让我修复 crypto.go。 + +让我重新生成 crypto.go 文件: + +// File: ymt_v3/crypto.go +```go +package ymt_v3 + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "reflect" + "sort" + "strings" + "time" + + "github.com/tjfoc/gmsm/sm4" +) + +// EncryptType 加密类型 +type EncryptType string + +const ( + EncryptTypeAES EncryptType = "aes" + EncryptTypeSM4 EncryptType = "sm4" +) + +// ==================== RSA签名 ==================== + +// SignWithRSA 使用RSA私钥对数据进行签名 +func SignWithRSA(signStr string, privateKeyPEM string) (string, error) { + block, _ := pem.Decode([]byte(privateKeyPEM)) + if block == nil { + return "", fmt.Errorf("failed to decode PEM private key") + } + + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse private key: %v", err) + } + } + + rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey) + if !ok { + return "", fmt.Errorf("not a RSA private key") + } + + h := sha256.New() + h.Write([]byte(signStr)) + hashed := h.Sum(nil) + + signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(signature), nil +} + +// VerifyWithRSA 使用RSA公钥验证签名 +func VerifyWithRSA(signStr, signature string, publicKeyPEM string) error { + block, _ := pem.Decode([]byte(publicKeyPEM)) + if block == nil { + return fmt.Errorf("failed to decode PEM public key") + } + + publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse public key: %v", err) + } + + rsaPublicKey, ok := publicKey.(*rsa.PublicKey) + if !ok { + return fmt.Errorf("not a RSA public key") + } + + h := sha256.New() + h.Write([]byte(signStr)) + hashed := h.Sum(nil) + + sigBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return fmt.Errorf("failed to decode signature: %v", err) + } + + return rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed, sigBytes) +} + +// ==================== AES ECB加密 ==================== + +// AESECBEncrypt AES-ECB模式加密 +func AESECBEncrypt(plaintext []byte, key []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建AES cipher失败: %v", err) + } + + padded := pkcs7Padding(plaintext, aes.BlockSize) + + ciphertext := make([]byte, len(padded)) + for i := 0; i < len(padded); i += aes.BlockSize { + block.Encrypt(ciphertext[i:i+aes.BlockSize], padded[i:i+aes.BlockSize]) + } + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// AESECBDecrypt AES-ECB模式解密 +func AESECBDecrypt(encryptedData string, key []byte) ([]byte, error) { + ciphertext, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("解码失败: %v", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("创建AES cipher失败: %v", err) + } + + if len(ciphertext)%aes.BlockSize != 0 { + return nil, fmt.Errorf("密文长度不是块大小的整数倍") + } + + plaintext := make([]byte, len(ciphertext)) + for i := 0; i < len(ciphertext); i += aes.BlockSize { + block.Decrypt(plaintext[i:i+aes.BlockSize], ciphertext[i:i+aes.BlockSize]) + } + + plaintext, err = pkcs7UnPadding(plaintext) + if err != nil { + return nil, fmt.Errorf("去除填充失败: %v", err) + } + + return plaintext, nil +} + +// ==================== SM4 CBC加密 ==================== + +// SM4CBCEncrypt SM4-CBC模式加密 +func SM4CBCEncrypt(plaintext []byte, key []byte) (string, error) { + if len(key) != 16 { + return "", fmt.Errorf("SM4密钥长度必须为16字节") + } + + block, err := sm4.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + padded := pkcs7Padding(plaintext, block.BlockSize()) + + iv := make([]byte, block.BlockSize()) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", fmt.Errorf("生成IV失败: %v", err) + } + + mode := cipher.NewCBCEncrypter(block, iv) + ciphertext := make([]byte, len(padded)) + mode.CryptBlocks(ciphertext, padded) + + result := append(iv, ciphertext...) + return base64.StdEncoding.EncodeToString(result), nil +} + +// SM4CBCDecrypt SM4-CBC模式解密 +func SM4CBCDecrypt(encryptedData string, key []byte) ([]byte, error) { + if len(key) != 16 { + return nil, fmt.Errorf("SM4密钥长度必须为16字节") + } + + data, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("解码失败: %v", err) + } + + block, err := sm4.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + blockSize := block.BlockSize() + if len(data) < blockSize { + return nil, fmt.Errorf("数据长度不足") + } + iv := data[:blockSize] + ciphertext := data[blockSize:] + + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + plaintext, err = pkcs7UnPadding(plaintext) + if err != nil { + return nil, fmt.Errorf("去除填充失败: %v", err) + } + + return plaintext, nil +} + +// ==================== PKCS7填充 ==================== + +// pkcs7Padding PKCS7填充 +func pkcs7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) +} + +// pkcs7UnPadding 去除PKCS7填充 +func pkcs7UnPadding(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, fmt.Errorf("数据为空") + } + padding := int(data[length-1]) + if padding > length || padding == 0 { + return nil, fmt.Errorf("无效的填充") + } + for i := length - padding; i < length; i++ { + if data[i] != byte(padding) { + return nil, fmt.Errorf("无效的填充") + } + } + return data[:length-padding], nil +} + +// ==================== 业务参数加密/解密 ==================== + +// EncryptBizParams 加密业务参数 +// 1. 将业务参数去掉"零"值的参数,再由小到大按照字母排序,转成json字符串得到plaintext +// 2. 使用应用key将plaintext字符串加密,得到ciphertext +func EncryptBizParams(params interface{}, key []byte, encType EncryptType) (string, error) { + plaintext, err := marshalWithoutZero(params) + if err != nil { + return "", fmt.Errorf("序列化业务参数失败: %v", err) + } + + switch encType { + case EncryptTypeAES: + return AESECBEncrypt([]byte(plaintext), key) + case EncryptTypeSM4: + return SM4CBCEncrypt([]byte(plaintext), key) + default: + return "", fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// DecryptBizParams 解密业务参数 +func DecryptBizParams(ciphertext string, key []byte, encType EncryptType) ([]byte, error) { + switch encType { + case EncryptTypeAES: + return AESECBDecrypt(ciphertext, key) + case EncryptTypeSM4: + return SM4CBCDecrypt(ciphertext, key) + default: + return nil, fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// EncryptBizParamsRaw 直接加密字节数据 +func EncryptBizParamsRaw(plaintext []byte, key []byte, encType EncryptType) (string, error) { + switch encType { + case EncryptTypeAES: + return AESECBEncrypt(plaintext, key) + case EncryptTypeSM4: + return SM4CBCEncrypt(plaintext, key) + default: + return "", fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// marshalWithoutZero 将结构体序列化为JSON,去掉零值字段并按字母排序 +func marshalWithoutZero(v interface{}) (string, error) { + val := reflect.ValueOf(v) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + if val.Kind() == reflect.Map { + return marshalMapSorted(v) + } + data, err := json.Marshal(v) + return string(data), err + } + + result := make(map[string]interface{}) + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + name := strings.Split(jsonTag, ",")[0] + + if isZeroValue(field) { + continue + } + + result[name] = field.Interface() + } + + return marshalMapSorted(result) +} + +// marshalMapSorted 将map按键排序后序列化为JSON +func marshalMapSorted(m map[string]interface{}) (string, error) { + keys := make([]string, 0, len(m)) + for k := range m { + 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(m[k]) + if err != nil { + return "", err + } + buf.Write(valBytes) + } + buf.WriteByte('}') + + return buf.String(), nil +} + +// isZeroValue 判断reflect.Value是否为零值 +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String: + return v.String() == "" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Slice, reflect.Map, reflect.Ptr: + return v.IsNil() + default: + return false + } +} + +// ==================== 签名相关 ==================== + +// BuildSignStr 构建签名字符串 +// 规则:app_id + timestamp + ciphertext +func BuildSignStr(appID, timestamp, ciphertext string) string { + return appID + timestamp + ciphertext +} + +// GenerateTimestamp 生成时间戳,格式 yyyy-MM-dd HH:mm:ss +func GenerateTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +// ==================== 回调验签 ==================== + +// VerifyNotifySign 验证回调通知签名 +func VerifyNotifySign(header *NotifyHeader, data interface{}, key []byte, publicKeyPEM string, encType EncryptType) error { + plaintext, err := marshalWithoutZero(data) + if err != nil { + return fmt.Errorf("序列化回调数据失败: %v", err) + } + + ciphertext, err := EncryptBizParamsRaw([]byte(plaintext), key, encType) + if err != nil { + return fmt.Errorf("加密回调数据失败: %v", err) + } + + signStr := BuildSignStr(header.Appid, header.Timestamp, ciphertext) + + return VerifyWithRSA(signStr, header.Sign, publicKeyPEM) +} +``` + +现在让我重新生成完整的 client.go 文件,确保 crypto.go 中的函数签名与 client.go 中的调用一致。 + +实际上,我注意到 crypto.go 中我使用了 `cipher.NewCBCEncrypter` 和 `cipher.NewCBCDecrypter`,这些是标准库 `crypto/cipher` 包中的函数,它们接受 `cipher.Block` 接口。而 `sm4.NewCipher` 返回的正是 `cipher.Block` 接口,所以可以直接使用。 + +让我检查一下所有文件的一致性,然后输出最终版本。 + +好的,所有文件已经生成完毕。让我输出最终版本。 + +实际上,让我重新审视一下 crypto.go 中的 CBC 实现。标准库的 `cipher.NewCBCEncrypter` 和 `cipher.NewCBCDecrypter` 接受 `cipher.Block` 接口,而 `sm4.NewCipher` 返回 `*sm4.SM4Cipher` 实现了 `cipher.Block` 接口,所以可以直接使用。 + +但是,SM4 CBC 加密时,IV 需要前置到密文中,解密时从密文中提取 IV。这个逻辑已经在代码中实现了。 + +好的,所有文件都准备好了,让我输出最终版本。 + +等等,让我再检查一下 go.mod 中的依赖。`github.com/tjfoc/gmsm` 的版本,我写的是 `v1.4.1`,但实际最新版本可能不同。不过对于 SDK 来说,指定一个合理的版本即可。 + +好的,现在输出所有文件。现在让我重新生成完整的 crypto.go 文件,修复 CBC 实现问题: + +// File: ymt_v3/crypto.go +```go +package ymt_v3 + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "reflect" + "sort" + "strings" + "time" + + "github.com/tjfoc/gmsm/sm4" +) + +// EncryptType 加密类型 +type EncryptType string + +const ( + // EncryptTypeAES AES-ECB加密 + EncryptTypeAES EncryptType = "aes" + // EncryptTypeSM4 SM4-CBC加密 + EncryptTypeSM4 EncryptType = "sm4" +) + +// ==================== RSA签名 ==================== + +// SignWithRSA 使用RSA私钥对数据进行签名 +func SignWithRSA(signStr string, privateKeyPEM string) (string, error) { + block, _ := pem.Decode([]byte(privateKeyPEM)) + if block == nil { + return "", fmt.Errorf("failed to decode PEM private key") + } + + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse private key: %v", err) + } + } + + rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey) + if !ok { + return "", fmt.Errorf("not a RSA private key") + } + + h := sha256.New() + h.Write([]byte(signStr)) + hashed := h.Sum(nil) + + signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(signature), nil +} + +// VerifyWithRSA 使用RSA公钥验证签名 +func VerifyWithRSA(signStr, signature string, publicKeyPEM string) error { + block, _ := pem.Decode([]byte(publicKeyPEM)) + if block == nil { + return fmt.Errorf("failed to decode PEM public key") + } + + publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse public key: %v", err) + } + + rsaPublicKey, ok := publicKey.(*rsa.PublicKey) + if !ok { + return fmt.Errorf("not a RSA public key") + } + + h := sha256.New() + h.Write([]byte(signStr)) + hashed := h.Sum(nil) + + sigBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return fmt.Errorf("failed to decode signature: %v", err) + } + + return rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed, sigBytes) +} + +// ==================== AES ECB加密 ==================== + +// AESECBEncrypt AES-ECB模式加密 +func AESECBEncrypt(plaintext []byte, key []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建AES cipher失败: %v", err) + } + + padded := pkcs7Padding(plaintext, aes.BlockSize) + + ciphertext := make([]byte, len(padded)) + for i := 0; i < len(padded); i += aes.BlockSize { + block.Encrypt(ciphertext[i:i+aes.BlockSize], padded[i:i+aes.BlockSize]) + } + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// AESECBDecrypt AES-ECB模式解密 +func AESECBDecrypt(encryptedData string, key []byte) ([]byte, error) { + ciphertext, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("解码失败: %v", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("创建AES cipher失败: %v", err) + } + + if len(ciphertext)%aes.BlockSize != 0 { + return nil, fmt.Errorf("密文长度不是块大小的整数倍") + } + + plaintext := make([]byte, len(ciphertext)) + for i := 0; i < len(ciphertext); i += aes.BlockSize { + block.Decrypt(plaintext[i:i+aes.BlockSize], ciphertext[i:i+aes.BlockSize]) + } + + plaintext, err = pkcs7UnPadding(plaintext) + if err != nil { + return nil, fmt.Errorf("去除填充失败: %v", err) + } + + return plaintext, nil +} + +// ==================== SM4 CBC加密 ==================== + +// SM4CBCEncrypt SM4-CBC模式加密 +// IV会前置到密文中,解密时自动提取 +func SM4CBCEncrypt(plaintext []byte, key []byte) (string, error) { + if len(key) != 16 { + return "", fmt.Errorf("SM4密钥长度必须为16字节") + } + + block, err := sm4.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + padded := pkcs7Padding(plaintext, block.BlockSize()) + + iv := make([]byte, block.BlockSize()) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", fmt.Errorf("生成IV失败: %v", err) + } + + mode := cipher.NewCBCEncrypter(block, iv) + ciphertext := make([]byte, len(padded)) + mode.CryptBlocks(ciphertext, padded) + + // IV前置到密文 + result := append(iv, ciphertext...) + return base64.StdEncoding.EncodeToString(result), nil +} + +// SM4CBCDecrypt SM4-CBC模式解密 +// 从密文中提取前置的IV进行解密 +func SM4CBCDecrypt(encryptedData string, key []byte) ([]byte, error) { + if len(key) != 16 { + return nil, fmt.Errorf("SM4密钥长度必须为16字节") + } + + data, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("解码失败: %v", err) + } + + block, err := sm4.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + blockSize := block.BlockSize() + if len(data) < blockSize*2 { + return nil, fmt.Errorf("数据长度不足") + } + iv := data[:blockSize] + ciphertext := data[blockSize:] + + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + plaintext, err = pkcs7UnPadding(plaintext) + if err != nil { + return nil, fmt.Errorf("去除填充失败: %v", err) + } + + return plaintext, nil +} + +// ==================== PKCS7填充 ==================== + +// pkcs7Padding PKCS7填充 +func pkcs7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) +} + +// pkcs7UnPadding 去除PKCS7填充 +func pkcs7UnPadding(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, fmt.Errorf("数据为空") + } + padding := int(data[length-1]) + if padding > length || padding == 0 { + return nil, fmt.Errorf("无效的填充") + } + for i := length - padding; i < length; i++ { + if data[i] != byte(padding) { + return nil, fmt.Errorf("无效的填充") + } + } + return data[:length-padding], nil +} + +// ==================== 业务参数加密/解密 ==================== + +// EncryptBizParams 加密业务参数 +// 1. 将业务参数去掉"零"值的参数,再由小到大按照字母排序,转成json字符串得到plaintext +// 2. 使用应用key将plaintext字符串加密,得到ciphertext +func EncryptBizParams(params interface{}, key []byte, encType EncryptType) (string, error) { + plaintext, err := marshalWithoutZero(params) + if err != nil { + return "", fmt.Errorf("序列化业务参数失败: %v", err) + } + + switch encType { + case EncryptTypeAES: + return AESECBEncrypt([]byte(plaintext), key) + case EncryptTypeSM4: + return SM4CBCEncrypt([]byte(plaintext), key) + default: + return "", fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// DecryptBizParams 解密业务参数 +func DecryptBizParams(ciphertext string, key []byte, encType EncryptType) ([]byte, error) { + switch encType { + case EncryptTypeAES: + return AESECBDecrypt(ciphertext, key) + case EncryptTypeSM4: + return SM4CBCDecrypt(ciphertext, key) + default: + return nil, fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// EncryptBizParamsRaw 直接加密字节数据(用于回调验签) +func EncryptBizParamsRaw(plaintext []byte, key []byte, encType EncryptType) (string, error) { + switch encType { + case EncryptTypeAES: + return AESECBEncrypt(plaintext, key) + case EncryptTypeSM4: + return SM4CBCEncrypt(plaintext, key) + default: + return "", fmt.Errorf("不支持的加密类型: %s", encType) + } +} + +// marshalWithoutZero 将结构体序列化为JSON,去掉零值字段并按字母排序 +func marshalWithoutZero(v interface{}) (string, error) { + val := reflect.ValueOf(v) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + if val.Kind() == reflect.Map { + return marshalMapSorted(v) + } + data, err := json.Marshal(v) + return string(data), err + } + + result := make(map[string]interface{}) + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + name := strings.Split(jsonTag, ",")[0] + + if isZeroValue(field) { + continue + } + + result[name] = field.Interface() + } + + return marshalMapSorted(result) +} + +// marshalMapSorted 将map按键排序后序列化为JSON +func marshalMapSorted(m map[string]interface{}) (string, error) { + keys := make([]string, 0, len(m)) + for k := range m { + 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(m[k]) + if err != nil { + return "", err + } + buf.Write(valBytes) + } + buf.WriteByte('}') + + return buf.String(), nil +} + +// isZeroValue 判断reflect.Value是否为零值 +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String: + return v.String() == "" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Slice, reflect.Map, reflect.Ptr: + return v.IsNil() + default: + return false + } +} + +// ==================== 签名相关 ==================== + +// BuildSignStr 构建签名字符串 +// 规则:app_id + timestamp + ciphertext +func BuildSignStr(appID, timestamp, ciphertext string) string { + return appID + timestamp + ciphertext +} + +// GenerateTimestamp 生成时间戳,格式 yyyy-MM-dd HH:mm:ss +func GenerateTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +// ==================== 回调验签 ==================== + +// VerifyNotifySign 验证回调通知签名 +// 1. 获取header头里面的签名信息 +// 2. 获取body里面的业务参数data +// 3. 将业务参数data去掉"零"值的参数,再由小到大按照字母排序,转成json字符串得到plaintext +// 4. 使用应用key将plaintext字符串加密得到ciphertext +// 5. 拼接签名字符串:app_id + timestamp + ciphertext +// 6. 使用应用公钥验签 +func VerifyNotifySign(header *NotifyHeader, data interface{}, key []byte, publicKeyPEM string, encType Encrypt + +## 加密实现 + +以下是从加密工具获取的完整实现: + +### 加密实现 1 + +{ + "success": true, + "data": "\n### 时间戳和随机数生成规范\n\n**代码模板**:\n\n```go\npackage crypto\n\nimport (\n \"crypto/rand\"\n \"fmt\"\n \"math/big\"\n \"time\"\n)\n\n// GenerateTimestamp 生成秒级时间戳\nfunc GenerateTimestamp() string {\n return fmt.Sprintf(\"%d\", time.Now().Unix())\n}\n\n// GenerateTimestampMillis 生成毫秒级时间戳\nfunc GenerateTimestampMillis() string {\n return fmt.Sprintf(\"%d\", time.Now().UnixMilli())\n}\n\n// GenerateNonce 生成指定长度的随机字符串(加密安全)\nfunc GenerateNonce(length int) (string, error) {\n const charset = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n b := make([]byte, length)\n for i := range b {\n num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))\n if err != nil {\n return \"\", err\n }\n b[i] = charset[num.Int64()]\n }\n return string(b), nil\n}\n```\n\n**注意事项**:\n- 确认文档要求的是秒级还是毫秒级时间戳\n- 确认nonce的长度要求(通常16-32位)\n- 生产环境建议使用加密安全的随机数生成器\n", + "tool": "nonce_timestamp" +} + +### 加密实现 2 + +{ + "success": true, + "data": "\n### AES-ECB 加密完整实现指南\n\n**配置参数**:\n- 密钥长度: 16 字节\n- 编码方式: base64\n\n**完整代码模板**:\n\n```go\npackage crypto\n\nimport (\n \"crypto/aes\"\n \"encoding/base64\"\n \"encoding/hex\"\n \"fmt\"\n \"bytes\"\n)\n\n// AESECBEncrypt AES-ECB模式加密\nfunc AESECBEncrypt(plaintext []byte, key []byte, encoding string) (string, error) {\n block, err := aes.NewCipher(key)\n if err != nil {\n return \"\", fmt.Errorf(\"创建AES cipher失败: %v\", err)\n }\n\n padded := pkcs7Padding(plaintext, aes.BlockSize)\n\n ciphertext := make([]byte, len(padded))\n for i := 0; i \u003c len(padded); i += aes.BlockSize {\n block.Encrypt(ciphertext[i:i+aes.BlockSize], padded[i:i+aes.BlockSize])\n }\n\n if encoding == \"hex\" {\n return hex.EncodeToString(ciphertext), nil\n }\n return base64.StdEncoding.EncodeToString(ciphertext), nil\n}\n\n// AESECBDecrypt AES-ECB模式解密\nfunc AESECBDecrypt(encryptedData string, key []byte, encoding string) ([]byte, error) {\n var ciphertext []byte\n var err error\n \n if encoding == \"hex\" {\n ciphertext, err = hex.DecodeString(encryptedData)\n } else {\n ciphertext, err = base64.StdEncoding.DecodeString(encryptedData)\n }\n if err != nil {\n return nil, fmt.Errorf(\"解码失败: %v\", err)\n }\n\n block, err := aes.NewCipher(key)\n if err != nil {\n return nil, fmt.Errorf(\"创建AES cipher失败: %v\", err)\n }\n\n if len(ciphertext)%aes.BlockSize != 0 {\n return nil, fmt.Errorf(\"密文长度不是块大小的整数倍\")\n }\n\n plaintext := make([]byte, len(ciphertext))\n for i := 0; i \u003c len(ciphertext); i += aes.BlockSize {\n block.Decrypt(plaintext[i:i+aes.BlockSize], ciphertext[i:i+aes.BlockSize])\n }\n\n plaintext, err = pkcs7UnPadding(plaintext)\n if err != nil {\n return nil, fmt.Errorf(\"去除填充失败: %v\", err)\n }\n\n return plaintext, nil\n}\n```\n\n%!(EXTRA string=16)", + "tool": "aes_ecb_encrypt" +} + +### 加密实现 3 + +{ + "success": true, + "data": "\n### RSA签名完整实现指南\n\n**适用场景**:文档要求使用RSA私钥对请求参数进行签名\n\n**配置参数**:\n- 哈希算法: SHA256\n- 编码方式: base64\n\n**完整代码模板**:\n```go\npackage crypto\n\nimport (\n \"crypto\"\n \"crypto/rand\"\n \"crypto/rsa\"\n \"crypto/sha256\"\n \"crypto/sha512\"\n \"crypto/x509\"\n \"encoding/base64\"\n \"encoding/hex\"\n \"encoding/pem\"\n \"fmt\"\n \"sort\"\n \"strings\"\n)\n\n// RSAConfig RSA签名配置\ntype RSAConfig struct {\n PrivateKeyPEM string // PEM格式的私钥\n KeyID string // 密钥ID(如文档要求携带)\n}\n\n// SignWithRSA 使用RSA私钥对请求进行签名\nfunc SignWithRSA(params map[string]string, config *RSAConfig) (string, error) {\n // 1. 拼接参数(按字典序排序)\n keys := make([]string, 0, len(params))\n for k := range params {\n if k != \"sign\" \u0026\u0026 k != \"signature\" {\n keys = append(keys, k)\n }\n }\n sort.Strings(keys)\n \n var sb strings.Builder\n for _, k := range keys {\n if sb.Len() \u003e 0 {\n sb.WriteString(\"\u0026\")\n }\n sb.WriteString(k)\n sb.WriteString(\"=\")\n sb.WriteString(params[k])\n }\n signStr := sb.String()\n \n // 2. 加载私钥\n block, _ := pem.Decode([]byte(config.PrivateKeyPEM))\n if block == nil {\n return \"\", fmt.Errorf(\"failed to decode PEM private key\")\n }\n \n privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)\n if err != nil {\n // 尝试PKCS1格式\n privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)\n if err != nil {\n return \"\", fmt.Errorf(\"failed to parse private key: %v\", err)\n }\n }\n \n rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)\n if !ok {\n return \"\", fmt.Errorf(\"not a RSA private key\")\n }\n \n // 3. 计算哈希\n var hashed []byte\n var hashFunc crypto.Hash\n \n switch \"SHA256\" {\n case \"SHA384\":\n h := sha512.New384()\n h.Write([]byte(signStr))\n hashed = h.Sum(nil)\n hashFunc = crypto.SHA384\n case \"SHA512\":\n h := sha512.New()\n h.Write([]byte(signStr))\n hashed = h.Sum(nil)\n hashFunc = crypto.SHA512\n default: // SHA256\n h := sha256.New()\n h.Write([]byte(signStr))\n hashed = h.Sum(nil)\n hashFunc = crypto.SHA256\n }\n \n // 4. RSA签名\n signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, hashFunc, hashed)\n if err != nil {\n return \"\", err\n }\n \n // 5. 编码\n if \"base64\" == \"hex\" {\n return hex.EncodeToString(signature), nil\n }\n return base64.StdEncoding.EncodeToString(signature), nil\n}\n```\n\n**注意事项**:\n- 确认文档使用的是 PKCS1 还是 PKCS8 格式\n- 确认是否需要添加额外的固定盐值\n- 确认编码是 Base64 还是 Hex\n- 注意排除签名字段本身(sign/signature)\n", + "tool": "rsa_sign" +} + +### 加密实现 4 + +{ + "success": true, + "data": "\n### SM4-CBC 国密对称加密完整实现指南\n\n**⚠️ 重要:必须使用第三方库,不要自己实现 SM4 算法!**\n**推荐使用:github.com/tjfoc/gmsm**\n\n**前置要求**:\n```bash\ngo get github.com/tjfoc/gmsm\n```\n\n**配置参数**:\n- 编码方式: base64\n\n**完整代码模板(请直接复制使用)**:\n\n```go\npackage crypto\n\nimport (\n \"crypto/cipher\"\n \"crypto/rand\"\n \"encoding/base64\"\n \"encoding/hex\"\n \"fmt\"\n \"io\"\n \"bytes\"\n\n \"github.com/tjfoc/gmsm/sm4\" // ⚠️ 必须使用此库,不要自己实现SM4\n)\n\n// SM4CBCEncrypt SM4-CBC模式加密\n// 使用 github.com/tjfoc/gmsm/sm4 实现\nfunc SM4CBCEncrypt(plaintext []byte, key []byte) (string, error) {\n if len(key) != 16 {\n return \"\", fmt.Errorf(\"SM4密钥长度必须为16字节\")\n }\n\n // 使用 gmsm 库创建 SM4 cipher\n block, err := sm4.NewCipher(key)\n if err != nil {\n return \"\", fmt.Errorf(\"创建SM4 cipher失败: %v\", err)\n }\n\n padded := pkcs7Padding(plaintext, block.BlockSize())\n\n iv := make([]byte, block.BlockSize())\n if _, err := io.ReadFull(rand.Reader, iv); err != nil {\n return \"\", fmt.Errorf(\"生成IV失败: %v\", err)\n }\n\n mode := cipher.NewCBCEncrypter(block, iv)\n ciphertext := make([]byte, len(padded))\n mode.CryptBlocks(ciphertext, padded)\n\n result := append(iv, ciphertext...)\n if \"base64\" == \"hex\" {\n return hex.EncodeToString(result), nil\n }\n return base64.StdEncoding.EncodeToString(result), nil\n}\n\n// SM4CBCDecrypt SM4-CBC模式解密\n// 使用 github.com/tjfoc/gmsm/sm4 实现\nfunc SM4CBCDecrypt(encryptedData string, key []byte) ([]byte, error) {\n if len(key) != 16 {\n return nil, fmt.Errorf(\"SM4密钥长度必须为16字节\")\n }\n\n var data []byte\n var err error\n if \"base64\" == \"hex\" {\n data, err = hex.DecodeString(encryptedData)\n } else {\n data, err = base64.StdEncoding.DecodeString(encryptedData)\n }\n if err != nil {\n return nil, fmt.Errorf(\"解码失败: %v\", err)\n }\n\n block, err := sm4.NewCipher(key)\n if err != nil {\n return nil, fmt.Errorf(\"创建SM4 cipher失败: %v\", err)\n }\n\n blockSize := block.BlockSize()\n if len(data) \u003c blockSize {\n return nil, fmt.Errorf(\"数据长度不足\")\n }\n iv := data[:blockSize]\n ciphertext := data[blockSize:]\n\n mode := cipher.NewCBCDecrypter(block, iv)\n plaintext := make([]byte, len(ciphertext))\n mode.CryptBlocks(plaintext, ciphertext)\n\n plaintext, err = pkcs7UnPadding(plaintext)\n if err != nil {\n return nil, fmt.Errorf(\"去除填充失败: %v\", err)\n }\n\n return plaintext, nil\n}\n\n// pkcs7Padding PKCS7填充\nfunc pkcs7Padding(data []byte, blockSize int) []byte {\n padding := blockSize - len(data)%blockSize\n padText := bytes.Repeat([]byte{byte(padding)}, padding)\n return append(data, padText...)\n}\n\n// pkcs7UnPadding 去除PKCS7填充\nfunc pkcs7UnPadding(data []byte) ([]byte, error) {\n length := len(data)\n if length == 0 {\n return nil, fmt.Errorf(\"数据为空\")\n }\n padding := int(data[length-1])\n if padding \u003e length || padding == 0 {\n return nil, fmt.Errorf(\"无效的填充\")\n }\n for i := length - padding; i \u003c length; i++ {\n if data[i] != byte(padding) {\n return nil, fmt.Errorf(\"无效的填充\")\n }\n }\n return data[:length-padding], nil\n}\n```\n\n**⚠️ 重要提醒**:\n1. **不要自己实现 SM4 算法**,请直接使用 github.com/tjfoc/gmsm\n2. 这个库是国密官方推荐的 Go 实现,安全可靠\n3. 在 go.mod 中添加依赖:`require github.com/tjfoc/gmsm v1.4.1`\n\n**注意事项**:\n- SM4 密钥固定为 16 字节\n- IV 必须随机生成且每次不同\n- CBC 模式需要 IV,ECB 模式不需要\n", + "tool": "sm4_cbc_encrypt" +} + +### 加密实现 5 + +{ + "success": true, + "data": "\n### 参数拼接规范\n\n**常见拼接方式**:\n\n1. **字典序排序**:按key字典序排序后拼接 `key=value\u0026key2=value2`\n2. **固定顺序**:按文档指定顺序拼接\n3. **JSON字符串**:整个请求体作为签名串\n\n**代码模板**:\n\n```go\npackage crypto\n\nimport (\n \"fmt\"\n \"sort\"\n \"strings\"\n)\n\n// BuildSignString 方式1:字典序排序拼接\nfunc BuildSignString(params map[string]string) string {\n keys := make([]string, 0, len(params))\n for k, v := range params {\n if v != \"\" \u0026\u0026 k != \"sign\" \u0026\u0026 k != \"signature\" {\n keys = append(keys, k)\n }\n }\n sort.Strings(keys)\n \n var parts []string\n for _, k := range keys {\n parts = append(parts, fmt.Sprintf(\"%s=%s\", k, params[k]))\n }\n return strings.Join(parts, \"\u0026\")\n}\n\n// BuildSignStringOrdered 方式2:固定顺序拼接\nfunc BuildSignStringOrdered(params map[string]string, orderedKeys []string) string {\n var parts []string\n for _, k := range orderedKeys {\n if v, ok := params[k]; ok \u0026\u0026 v != \"\" {\n parts = append(parts, fmt.Sprintf(\"%s=%s\", k, v))\n }\n }\n return strings.Join(parts, \"\u0026\")\n}\n```\n\n**注意事项**:\n- 确认文档指定的排序规则(字典序/固定顺序)\n- 确认空值是否要包含(通常排除空值)\n- 确认是否需要URL编码\n- 注意排除签名字段本身\n", + "tool": "param_concat" +} +