xy_sh-20260724180223/xy_sh/generate.md

892 lines
34 KiB
Markdown
Raw 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.

现在我来生成所有 6 个文件。根据文档签名规则是将毫秒级时间戳timestamp字符串与encryptedData字符串直接拼接使用SM3算法结合预先约定的SM3 salt进行加密sm3WithSalt最终输出十六进制字符串作为sign值。
文档中的Java示例
```java
String sign = SmUtil.sm3WithSalt(sm3Salt.getBytes()).digestHex(timestamp+ encryptedData);
```
这里的 `sm3WithSalt` 是 Hutool 库中的方法,它实际上是在 SM3 哈希过程中加入了盐值。在 Go 中,我们可以通过将 salt 与数据拼接后计算 SM3 哈希来实现类似效果,或者使用 HMAC-SM3 的方式。
实际上,`sm3WithSalt` 在 Hutool 中的实现是:将 salt 作为 HMAC 的 key对数据进行 HMAC-SM3 计算。所以我们可以使用 HMAC-SM3 来实现。
现在生成所有文件:
1. go.mod
2. client.go
3. types.go
4. crypto.go
5. errors.go
6. example_test.go
Let me generate all files now.</think>现在我来生成所有 6 个文件。
// File: xy_sh/go.mod
```go
module xy_sh
go 1.21
require github.com/tjfoc/gmsm v1.4.1
```
// File: xy_sh/errors.go
```go
package xy_sh
import "fmt"
// ErrorCode 定义错误码类型
type ErrorCode int
const (
// ErrCodeSuccess 请求成功
ErrCodeSuccess ErrorCode = 0
// ErrCodeFail 请求失败
ErrCodeFail ErrorCode = -1
)
// SDKError SDK自定义错误
type SDKError struct {
Code ErrorCode
Message string
Err error
}
func (e *SDKError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *SDKError) Unwrap() error {
return e.Err
}
// NewSDKError 创建SDK错误
func NewSDKError(code ErrorCode, message string, err error) *SDKError {
return &SDKError{
Code: code,
Message: message,
Err: err,
}
}
var (
// ErrInvalidConfig 无效配置
ErrInvalidConfig = NewSDKError(ErrCodeFail, "无效配置", nil)
// ErrEncryptFailed 加密失败
ErrEncryptFailed = NewSDKError(ErrCodeFail, "加密失败", nil)
// ErrDecryptFailed 解密失败
ErrDecryptFailed = NewSDKError(ErrCodeFail, "解密失败", nil)
// ErrSignFailed 签名失败
ErrSignFailed = NewSDKError(ErrCodeFail, "签名失败", nil)
// ErrRequestFailed 请求失败
ErrRequestFailed = NewSDKError(ErrCodeFail, "请求失败", nil)
// ErrResponseParseFailed 响应解析失败
ErrResponseParseFailed = NewSDKError(ErrCodeFail, "响应解析失败", nil)
)
```
// File: xy_sh/types.go
```go
package xy_sh
// ============================================================
// 通用请求/响应结构体
// ============================================================
// BaseRequest 基础请求头参数
type BaseRequest struct {
Timestamp string `json:"-"` // 毫秒级时间戳由SDK自动填充
Sign string `json:"-"` // 签名由SDK自动生成
}
// EncryptedRequest 加密后的请求体
type EncryptedRequest struct {
EncryptedData string `json:"encryptedData"` // SM4加密后的业务数据
}
// EncryptedResponse 加密后的响应体
type EncryptedResponse struct {
Code int `json:"code"` // 返回状态编码0成功 -1失败
Msg string `json:"msg"` // 返回错误信息
Data *string `json:"data"` // JSON格式业务数据进行SM4加密后的字符串可能为null
}
// ============================================================
// 接口1卡券/直充权益下单接口
// ============================================================
// PlaceOrderRequest 下单请求业务参数
type PlaceOrderRequest struct {
ActCode string `json:"actCode"` // 活动code
GoodsCode string `json:"goodsCode"` // 供应商商品编号
ActOrderNum string `json:"actOrderNum"` // 活动方订单号,唯一
Account string `json:"account,omitempty"` // 充值账号
CallbackUrl string `json:"callbackUrl,omitempty"` // 回调地址
}
// PlaceOrderResponseData 下单响应解密后的业务数据
type PlaceOrderResponseData struct {
OrderNo string `json:"orderNo"` // 权益订单号
CouponNo string `json:"couponNo,omitempty"` // 卡号
CouponCode string `json:"couponCode,omitempty"` // 卡密
Status int `json:"status"` // 状态:-1发放中 0成功 1失败 2已核销 3已过期
ExpireTime string `json:"expireTime,omitempty"` // 有效期
}
// ============================================================
// 接口2卡券/直充/微信立减金订单查询接口
// ============================================================
// QueryOrderRequest 查询订单请求业务参数
type QueryOrderRequest struct {
ActCode string `json:"actCode"` // 活动code
OrderNo string `json:"orderNo"` // 供应商订单号
}
// CardInfo 卡券信息
type CardInfo struct {
CouponNo string `json:"couponNo"` // 卡号/短链
CouponCode string `json:"couponCode"` // 卡密
ExpireTime string `json:"expireTime"` // 过期时间
}
// QueryOrderResponseData 查询订单响应解密后的业务数据
type QueryOrderResponseData struct {
OrderNo string `json:"orderNo"` // 权益订单号
Status int `json:"status"` // 订单状态
Account string `json:"account,omitempty"` // 充值账号
CardInfo *CardInfo `json:"cardInfo,omitempty"` // 卡券信息
CouponId string `json:"couponId,omitempty"` // 优惠id
}
// ============================================================
// 接口3微信立减金订单充值接口
// ============================================================
// WechatRechargeRequest 微信立减金充值请求业务参数
type WechatRechargeRequest struct {
ActCode string `json:"actCode"` // 活动code
OrderNo string `json:"orderNo"` // 供应商订单号
GoodsCode string `json:"goodsCode"` // 供应商商品编号
ActOrderNum string `json:"actOrderNum"` // 活动方订单号
AppId string `json:"appId"` // 公众账号ID
OpenId string `json:"openId"` // 微信用户标识
CallbackUrl string `json:"callbackUrl,omitempty"` // 回调地址
}
// WechatRechargeResponseData 微信立减金充值响应解密后的业务数据
type WechatRechargeResponseData struct {
OrderNo string `json:"orderNo"` // 权益订单号
Status int `json:"status"` // 订单状态
CouponId string `json:"couponId,omitempty"` // 微信优惠id
}
// ============================================================
// 接口4卡券/直充/微信立减金充值结果通知接口
// ============================================================
// NotifyRequest 充值结果通知请求业务参数
type NotifyRequest struct {
OrderNo string `json:"orderNo"` // 供应商订单号
ActOrderNum string `json:"actOrderNum"` // 活动方订单号
Status int `json:"status"` // 订单状态
Account string `json:"account,omitempty"` // 直充账号
CardInfo *CardInfo `json:"cardInfo,omitempty"` // 卡券信息
CouponId string `json:"couponId,omitempty"` // 微信优惠id
}
// NotifyResponse 回调通知响应
type NotifyResponse struct {
StatusCode int // HTTP状态码
Body string // 响应体内容
}
```
// File: xy_sh/crypto.go
```go
package xy_sh
import (
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"time"
"bytes"
"github.com/tjfoc/gmsm/sm3"
"github.com/tjfoc/gmsm/sm4"
)
// ============================================================
// 时间戳生成
// ============================================================
// GenerateTimestampMillis 生成毫秒级时间戳字符串
func GenerateTimestampMillis() string {
return fmt.Sprintf("%d", time.Now().UnixMilli())
}
// ============================================================
// SM3 哈希(含盐值)
// ============================================================
// SM3WithSalt 使用SM3算法结合盐值进行哈希
// 对应Java Hutool的 SmUtil.sm3WithSalt(salt).digestHex(data)
// 实现方式将salt作为HMAC的key对数据进行HMAC-SM3计算
func SM3WithSalt(data []byte, salt []byte) string {
h := hmac.New(sm3.New, salt)
h.Write(data)
return hex.EncodeToString(h.Sum(nil))
}
// SM3WithSaltString 便捷方法对字符串进行SM3加盐哈希
func SM3WithSaltString(data string, salt string) string {
return SM3WithSalt([]byte(data), []byte(salt))
}
// ============================================================
// SM4-CBC 加密/解密
// ============================================================
// SM4Encrypt SM4-CBC模式加密返回base64编码字符串
// 使用 github.com/tjfoc/gmsm/sm4 实现
func SM4Encrypt(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
}
// SM4Decrypt SM4-CBC模式解密输入base64编码字符串返回明文
func SM4Decrypt(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("base64解码失败: %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
}
// 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
}
```
// File: xy_sh/client.go
```go
package xy_sh
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
// Client 供应商API客户端
type Client struct {
httpClient *http.Client
sm4Key []byte // SM4加密密钥16字节
sm3Salt string // SM3加盐哈希的盐值
baseURL string // 供应商接口基础地址
}
// ClientOption 客户端配置选项
type ClientOption func(*Client)
// WithHTTPClient 设置自定义HTTP客户端
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = httpClient
}
}
// NewClient 创建新的供应商API客户端
// sm4Key: SM4加密密钥必须是16字节
// sm3Salt: SM3加盐哈希的盐值
// baseURL: 供应商接口基础地址
func NewClient(sm4Key []byte, sm3Salt string, baseURL string, opts ...ClientOption) (*Client, error) {
if len(sm4Key) != 16 {
return nil, fmt.Errorf("%w: SM4密钥长度必须为16字节", ErrInvalidConfig)
}
if sm3Salt == "" {
return nil, fmt.Errorf("%w: SM3盐值不能为空", ErrInvalidConfig)
}
if baseURL == "" {
return nil, fmt.Errorf("%w: 接口地址不能为空", ErrInvalidConfig)
}
c := &Client{
httpClient: &http.Client{},
sm4Key: sm4Key,
sm3Salt: sm3Salt,
baseURL: baseURL,
}
for _, opt := range opts {
opt(c)
}
return c, nil
}
// ============================================================
// 内部方法:加密、签名、请求发送
// ============================================================
// encryptAndSign 对业务参数进行SM4加密并生成SM3签名
// 返回: encryptedData, timestamp, sign, error
func (c *Client) encryptAndSign(bizParams interface{}) (string, string, string, error) {
// 1. 将业务参数序列化为JSON
bizJSON, err := json.Marshal(bizParams)
if err != nil {
return "", "", "", fmt.Errorf("%w: 序列化业务参数失败: %v", ErrEncryptFailed, err)
}
// 2. SM4加密
encryptedData, err := SM4Encrypt(bizJSON, c.sm4Key)
if err != nil {
return "", "", "", fmt.Errorf("%w: %v", ErrEncryptFailed, err)
}
// 3. 生成毫秒级时间戳
timestamp := GenerateTimestampMillis()
// 4. 生成签名SM3(salt, timestamp + encryptedData)
signStr := timestamp + encryptedData
sign := SM3WithSaltString(signStr, c.sm3Salt)
return encryptedData, timestamp, sign, nil
}
// decryptResponseData 解密响应中的data字段
func (c *Client) decryptResponseData(encryptedData string, target interface{}) error {
// 1. SM4解密
plaintext, err := SM4Decrypt(encryptedData, c.sm4Key)
if err != nil {
return fmt.Errorf("%w: %v", ErrDecryptFailed, err)
}
// 2. JSON反序列化
if err := json.Unmarshal(plaintext, target); err != nil {
return fmt.Errorf("%w: 反序列化业务数据失败: %v", ErrResponseParseFailed, err)
}
return nil
}
// doRequest 发送加密请求并解密响应
func (c *Client) doRequest(ctx context.Context, url string, bizParams interface{}, responseData interface{}) (*EncryptedResponse, error) {
// 1. 加密业务参数并生成签名
encryptedData, timestamp, sign, err := c.encryptAndSign(bizParams)
if err != nil {
return nil, err
}
// 2. 构建请求体
reqBody := EncryptedRequest{
EncryptedData: encryptedData,
}
reqBodyJSON, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("%w: 序列化请求体失败: %v", ErrRequestFailed, err)
}
// 3. 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBodyJSON))
if err != nil {
return nil, fmt.Errorf("%w: 创建请求失败: %v", ErrRequestFailed, err)
}
// 4. 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("timestamp", timestamp)
req.Header.Set("sign", sign)
// 5. 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("%w: 发送请求失败: %v", ErrRequestFailed, err)
}
defer resp.Body.Close()
// 6. 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%w: 读取响应体失败: %v", ErrResponseParseFailed, err)
}
// 7. 解析响应
var encryptedResp EncryptedResponse
if err := json.Unmarshal(respBody, &encryptedResp); err != nil {
return nil, fmt.Errorf("%w: 解析响应JSON失败: %v", ErrResponseParseFailed, err)
}
// 8. 如果业务失败,直接返回
if encryptedResp.Code != 0 {
return &encryptedResp, nil
}
// 9. 如果data不为空解密data
if encryptedResp.Data != nil && *encryptedResp.Data != "" && responseData != nil {
if err := c.decryptResponseData(*encryptedResp.Data, responseData); err != nil {
return nil, err
}
}
return &encryptedResp, nil
}
// ============================================================
// 接口1卡券/直充权益下单接口
// ============================================================
// PlaceOrder 卡券/直充权益下单
// 适用于卡密,直充商品下单
func (c *Client) PlaceOrder(ctx context.Context, req *PlaceOrderRequest) (*EncryptedResponse, *PlaceOrderResponseData, error) {
url := c.baseURL + "/placeOrder"
var data PlaceOrderResponseData
resp, err := c.doRequest(ctx, url, req, &data)
if err != nil {
return nil, nil, err
}
if resp.Code != 0 {
return resp, nil, nil
}
return resp, &data, nil
}
// ============================================================
// 接口2卡券/直充/微信立减金订单查询接口
// ============================================================
// QueryOrder 查询订单状态
// 通过供应商订单号查询订单状态
func (c *Client) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*EncryptedResponse, *QueryOrderResponseData, error) {
url := c.baseURL + "/queryOrder"
var data QueryOrderResponseData
resp, err := c.doRequest(ctx, url, req, &data)
if err != nil {
return nil, nil, err
}
if resp.Code != 0 {
return resp, nil, nil
}
return resp, &data, nil
}
// ============================================================
// 接口3微信立减金订单充值接口
// ============================================================
// WechatRecharge 微信立减金订单充值
func (c *Client) WechatRecharge(ctx context.Context, req *WechatRechargeRequest) (*EncryptedResponse, *WechatRechargeResponseData, error) {
url := c.baseURL + "/wechatRecharge"
var data WechatRechargeResponseData
resp, err := c.doRequest(ctx, url, req, &data)
if err != nil {
return nil, nil, err
}
if resp.Code != 0 {
return resp, nil, nil
}
return resp, &data, nil
}
// ============================================================
// 接口4卡券/直充/微信立减金充值结果通知接口(服务端处理)
// ============================================================
// ParseNotifyRequest 解析回调通知请求
// 供应商主动推送充值结果通知时,行方使用此方法解析请求
// 返回: 解密后的通知请求参数, error
func (c *Client) ParseNotifyRequest(r *http.Request) (*NotifyRequest, error) {
// 1. 读取请求体
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %v", err)
}
defer r.Body.Close()
// 2. 解析请求体
var encryptedReq EncryptedRequest
if err := json.Unmarshal(body, &encryptedReq); err != nil {
return nil, fmt.Errorf("解析请求体JSON失败: %v", err)
}
// 3. 获取请求头中的timestamp和sign
timestamp := r.Header.Get("timestamp")
sign := r.Header.Get("sign")
// 4. 验证签名
expectedSign := SM3WithSaltString(timestamp+encryptedReq.EncryptedData, c.sm3Salt)
if sign != expectedSign {
return nil, fmt.Errorf("签名验证失败")
}
// 5. 解密业务数据
var notifyReq NotifyRequest
if err := c.decryptResponseData(encryptedReq.EncryptedData, &notifyReq); err != nil {
return nil, err
}
return &notifyReq, nil
}
// NotifyResponseOK 生成回调通知成功响应
// 行方处理成功后返回HTTP 200且内容为"ok"
func NotifyResponseOK() (int, string) {
return http.StatusOK, "ok"
}
// NotifyResponseFail 生成回调通知失败响应
func NotifyResponseFail() (int, string) {
return http.StatusInternalServerError, "fail"
}
```
// File: xy_sh/example_test.go
```go
package xy_sh
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"strings"
)
// ExampleClient_PlaceOrder 演示卡券/直充权益下单
func ExampleClient_PlaceOrder() {
// 模拟供应商服务端
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 读取请求体
var req EncryptedRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
// 验证签名(实际生产环境需要验证)
timestamp := r.Header.Get("timestamp")
sign := r.Header.Get("sign")
_ = timestamp
_ = sign
// 模拟响应
resp := EncryptedResponse{
Code: 0,
Msg: "请求成功",
Data: strPtr(`{"orderNo":"HM17575805323790000127073398f3772c00","expireTime":"2025-09-12 00:00:00","couponNo":"27073398f3772c00","couponCode":"27073398f3772c00","status":1}`),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
// 初始化客户端
// 注意SM4密钥必须为16字节
sm4Key := []byte("1234567890abcdef") // 16字节密钥
sm3Salt := "my_sm3_salt_value"
baseURL := server.URL
client, err := NewClient(sm4Key, sm3Salt, baseURL)
if err != nil {
log.Fatalf("创建客户端失败: %v", err)
}
// 调用下单接口
req := &PlaceOrderRequest{
ActCode: "ACT001",
GoodsCode: "123456",
ActOrderNum: "00001",
Account: "19912345678",
CallbackUrl: "https://xxx/notice",
}
resp, data, err := client.PlaceOrder(context.Background(), req)
if err != nil {
log.Fatalf("下单失败: %v", err)
}
if resp.Code != 0 {
fmt.Printf("下单失败: %s\n", resp.Msg)
return
}
fmt.Printf("订单号: %s\n", data.OrderNo)
fmt.Printf("卡号: %s\n", data.CouponNo)
fmt.Printf("卡密: %s\n", data.CouponCode)
fmt.Printf("状态: %d\n", data.Status)
fmt.Printf("有效期: %s\n", data.ExpireTime)
// Output:
// 订单号: HM17575805323790000127073398f3772c00
// 卡号: 27073398f3772c00
// 卡密: 27073398f3772c00
// 状态: 1
// 有效期: 2025-09-12 00:00:00
}
// ExampleClient_QueryOrder 演示订单查询
func ExampleClient_QueryOrder() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := EncryptedResponse{
Code: 0,
Msg: "请求成功",
Data: strPtr(`{"orderNo":"HM1757046717684000102707339840b23222","cardInfo":"{\"couponNo\":\"11111\",\"couponCode\":\"12334\",\"expireTime\":\"2029-03-09 00:00:00\"}","status":3}`),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client, _ := NewClient([]byte("1234567890abcdef"), "my_sm3_salt", server.URL)
req := &QueryOrderRequest{
ActCode: "FBG6vdYqGE4mGX7EH/woEg==",
OrderNo: "HM1757046717684000102707339840b23222",
}
resp, data, err := client.QueryOrder(context.Background(), req)
if err != nil {
log.Fatalf("查询失败: %v", err)
}
if resp.Code != 0 {
fmt.Printf("查询失败: %s\n", resp.Msg)
return
}
fmt.Printf("订单号: %s\n", data.OrderNo)
fmt.Printf("状态: %d\n", data.Status)
if data.CardInfo != nil {
fmt.Printf("卡号: %s\n", data.CardInfo.CouponNo)
fmt.Printf("卡密: %s\n", data.CardInfo.CouponCode)
fmt.Printf("过期时间: %s\n", data.CardInfo.ExpireTime)
}
// Output:
// 订单号: HM1757046717684000102707339840b23222
// 状态: 3
// 卡号: 11111
// 卡密: 12334
// 过期时间: 2029-03-09 00:00:00
}
// ExampleClient_WechatRecharge 演示微信立减金充值
func ExampleClient_WechatRecharge() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := EncryptedResponse{
Code: 0,
Msg: "请求成功",
Data: strPtr(`{"orderNo":"0001","couponId":"123456","status":1}`),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client, _ := NewClient([]byte("1234567890abcdef"), "my_sm3_salt", server.URL)
req := &WechatRechargeRequest{
ActCode: "FBG6vdYqGE4mGX7EH/woEg==",
GoodsCode: "0001",
ActOrderNum: "0001",
OpenId: "0001",
AppId: "00001",
}
resp, data, err := client.WechatRecharge(context.Background(), req)
if err != nil {
log.Fatalf("充值失败: %v", err)
}
if resp.Code != 0 {
fmt.Printf("充值失败: %s\n", resp.Msg)
return
}
fmt.Printf("订单号: %s\n", data.OrderNo)
fmt.Printf("优惠ID: %s\n", data.CouponId)
fmt.Printf("状态: %d\n", data.Status)
// Output:
// 订单号: 0001
// 优惠ID: 123456
// 状态: 1
}
// ExampleClient_ParseNotifyRequest 演示解析回调通知
func ExampleClient_ParseNotifyRequest() {
client, _ := NewClient([]byte("1234567890abcdef"), "my_sm3_salt", "http://example.com")
// 模拟供应商发来的回调通知
notifyData := NotifyRequest{
OrderNo: "HM202509291010001",
ActOrderNum: "XY2025092910100001",
Status: 3,
Account: "19912345678",
}
// 加密并签名(模拟供应商端操作)
encryptedData, timestamp, sign, err := client.encryptAndSign(notifyData)
if err != nil {
log.Fatalf("加密失败: %v", err)
}
// 构建HTTP请求
reqBody := fmt.Sprintf(`{"encryptedData":"%s"}`, encryptedData)
httpReq := httptest.NewRequest(http.MethodPost, "/notify", strings.NewReader(reqBody))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("timestamp", timestamp)
httpReq.Header.Set("sign", sign)
// 解析回调通知
notifyReq, err := client.ParseNotifyRequest(httpReq)
if err != nil {
log.Fatalf("解析回调通知失败: %v", err)
}
fmt.Printf("订单号: %s\n", notifyReq.OrderNo)
fmt.Printf("活动方订单号: %s\n", notifyReq.ActOrderNum)
fmt.Printf("状态: %d\n", notifyReq.Status)
fmt.Printf("账号: %s\n", notifyReq.Account)
// 生成成功响应
statusCode, body := NotifyResponseOK()
fmt.Printf("响应状态码: %d\n", statusCode)
fmt.Printf("响应内容: %s\n", body)
// Output:
// 订单号: HM202509291010001
// 活动方订单号: XY2025092910100001
// 状态: 3
// 账号: 19912345678
// 响应状态码: 200
// 响应内容: ok
}
// strPtr 辅助函数:返回字符串指针
func strPtr(s string) *string {
return &s
}
```
=== SDK 生成完成 ===
## 加密实现
以下是从加密工具获取的完整实现:
### 加密实现 1
{
"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"
}
### 加密实现 2
{
"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"
}
### 加密实现 3
{
"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 模式需要 IVECB 模式不需要\n",
"tool": "sm4_cbc_encrypt"
}
### 加密实现 4
{
"success": true,
"data": "\n### SM3 国密哈希算法完整实现指南\n\n**适用场景**:文档要求使用国密 SM3 算法进行摘要计算或签名验证\n\n**前置要求**:需要安装 github.com/tjfoc/gmsm\n\n```bash\ngo get github.com/tjfoc/gmsm\n```\n\n**配置参数**\n- 编码方式: hex\n- 包含 HMAC: true\n\n**完整代码模板**\n\n```go\npackage crypto\n\nimport (\n \"crypto/hmac\"\n \"encoding/base64\"\n \"encoding/hex\"\n \"fmt\"\n \"os\"\n \n \"github.com/tjfoc/gmsm/sm3\"\n)\n\n// SM3Hash 计算SM3哈希值\n// data: 待哈希的数据\n// encoding: 输出编码方式 (hex/base64)\n// 返回: 编码后的哈希值\nfunc SM3Hash(data []byte, encoding string) (string, error) {\n h := sm3.New()\n h.Write(data)\n hashBytes := h.Sum(nil)\n \n if encoding == \"base64\" {\n return base64.StdEncoding.EncodeToString(hashBytes), nil\n }\n return hex.EncodeToString(hashBytes), nil\n}\n\n// SM3HashString 计算字符串的SM3哈希值便捷方法\nfunc SM3HashString(data string, encoding string) (string, error) {\n return SM3Hash([]byte(data), encoding)\n}\n\n// SM3HashFile 计算文件的SM3哈希值\nfunc SM3HashFile(filePath string, encoding string) (string, error) {\n data, err := os.ReadFile(filePath)\n if err != nil {\n return \"\", fmt.Errorf(\"读取文件失败: %v\", err)\n }\n return SM3Hash(data, encoding)\n}\n\n// SM3Verify 验证数据与哈希值是否匹配\nfunc SM3Verify(data []byte, hash string, encoding string) (bool, error) {\n expected, err := SM3Hash(data, encoding)\n if err != nil {\n return false, err\n }\n return expected == hash, nil\n}\n\n// HMACSM3 HMAC-SM3计算\n// 使用标准HMAC算法底层使用SM3哈希函数\nfunc HMACSM3(data []byte, key []byte) string {\n // 使用标准HMAC底层哈希函数用SM3\n h := hmac.New(sm3.New, key)\n h.Write(data)\n return hex.EncodeToString(h.Sum(nil))\n}\n```\n\n**使用示例**\n```go\n// 计算字符串哈希\nhash, _ := SM3HashString(\"hello world\", \"hex\")\nfmt.Println(hash) // 输出64位十六进制字符串\n\n// 验证哈希\nvalid, _ := SM3Verify([]byte(\"hello world\"), hash, \"hex\")\nfmt.Println(valid) // true\n\n// HMAC-SM3示例\nkey := []byte(\"secret_key\")\ndata := []byte(\"hello world\")\nhmacResult := HMACSM3(data, key)\nfmt.Println(hmacResult)\n```\n\n**注意事项**\n- SM3 输出固定 256 位32字节的哈希值\n- 十六进制输出为 64 位字符串\n- 常用于数字签名、完整性校验等场景\n- SM3 是国密标准哈希算法,与 SHA-256 类似\n- HMAC-SM3 需要使用标准库 crypto/hmac底层哈希函数使用 sm3.New\n",
"tool": "sm3_hash"
}