xy_sh-20260724174214/xy_sh/generate.md

997 lines
36 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 个文件。
根据文档分析:
1. SM4-CBC 加密带IVIV拼在密文前面
2. SM3 with salt 签名(使用 sm3WithSalt即 HMAC-SM3
3. 毫秒级时间戳
4. 请求头携带 timestamp 和 sign请求体为 encryptedData
注意文档中的签名规则:
- 将毫秒级时间戳timestamp字符串与encryptedData字符串直接拼接
- 使用SM3算法结合预先约定的SM3 salt进行加密
- 最终输出十六进制字符串作为sign值
Java示例`SmUtil.sm3WithSalt(sm3Salt.getBytes()).digestHex(timestamp+encryptedData)`
这里的 sm3WithSalt 实际上是 HMAC-SM3salt 作为 key。
现在生成所有文件。
// 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"
// Error 自定义错误类型
type Error struct {
Code int `json:"code"`
Message string `json:"msg"`
}
func (e *Error) Error() string {
return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Message)
}
// IsSuccess 判断是否成功
func IsSuccess(code int) bool {
return code == 0
}
```
// File: xy_sh/types.go
```go
package xy_sh
// ===== 接口1卡券/直充权益下单接口 =====
// OrderRequest 下单请求业务参数
type OrderRequest struct {
ActCode string `json:"actCode"`
GoodsCode string `json:"goodsCode"`
ActOrderNum string `json:"actOrderNum"`
Account string `json:"account,omitempty"`
CallbackUrl string `json:"callbackUrl,omitempty"`
}
// OrderResponse 下单响应
type OrderResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *OrderData `json:"data"`
}
// OrderData 下单响应解密后数据
type OrderData struct {
OrderNo string `json:"orderNo"`
CouponNo string `json:"couponNo,omitempty"`
CouponCode string `json:"couponCode,omitempty"`
Status int `json:"status"`
ExpireTime string `json:"expireTime,omitempty"`
}
// ===== 接口2卡券/直充/微信立减金订单查询接口 =====
// QueryRequest 查询请求业务参数
type QueryRequest struct {
ActCode string `json:"actCode"`
OrderNo string `json:"orderNo"`
}
// QueryResponse 查询响应
type QueryResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *QueryData `json:"data"`
}
// QueryData 查询响应解密后数据
type QueryData struct {
OrderNo string `json:"orderNo"`
Status int `json:"status"`
Account string `json:"account,omitempty"`
CardInfo *CardInfo `json:"cardInfo,omitempty"`
CouponId string `json:"couponId,omitempty"`
}
// CardInfo 卡券信息
type CardInfo struct {
CouponNo string `json:"couponNo"`
CouponCode string `json:"couponCode"`
ExpireTime string `json:"expireTime"`
}
// ===== 接口3微信立减金订单充值接口 =====
// WechatRechargeRequest 微信立减金充值请求业务参数
type WechatRechargeRequest struct {
ActCode string `json:"actCode"`
OrderNo string `json:"orderNo"`
GoodsCode string `json:"goodsCode"`
ActOrderNum string `json:"actOrderNum"`
AppId string `json:"appId"`
OpenId string `json:"openId"`
CallbackUrl string `json:"callbackUrl,omitempty"`
}
// WechatRechargeResponse 微信立减金充值响应
type WechatRechargeResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *WechatRechargeData `json:"data"`
}
// WechatRechargeData 微信立减金充值响应解密后数据
type WechatRechargeData struct {
OrderNo string `json:"orderNo"`
Status int `json:"status"`
CouponId string `json:"couponId,omitempty"`
}
// ===== 接口4回调通知接口 =====
// CallbackRequest 回调通知请求业务参数
type CallbackRequest 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"`
}
// CallbackResponse 回调通知响应
type CallbackResponse struct {
StatusCode int
Body string
}
// ===== 通用加密请求/响应结构 =====
// EncryptedRequest 加密请求体
type EncryptedRequest struct {
EncryptedData string `json:"encryptedData"`
}
// EncryptedResponse 加密响应体
type EncryptedResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
}
```
Wait, I need to import encoding/json in types.go for json.RawMessage. Let me fix that.
Actually, let me reconsider the structure. The EncryptedResponse should have Data as a string (since it's an encrypted JSON string), and then we decrypt it. Let me look at the response format again:
```json
{
"code": 0,
"msg": "请求成功",
"data": "{\"orderNo\":\"HM17575805323790000127073398f3772c00\",...}"
}
```
So `data` is a JSON string (encrypted), not an object. So it should be `string` type.
Let me regenerate all files properly.
// File: xy_sh/types.go
```go
package xy_sh
// ===== 接口1卡券/直充权益下单接口 =====
// OrderRequest 下单请求业务参数
type OrderRequest struct {
ActCode string `json:"actCode"`
GoodsCode string `json:"goodsCode"`
ActOrderNum string `json:"actOrderNum"`
Account string `json:"account,omitempty"`
CallbackUrl string `json:"callbackUrl,omitempty"`
}
// OrderResponse 下单响应
type OrderResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *OrderData `json:"data"`
}
// OrderData 下单响应解密后数据
type OrderData struct {
OrderNo string `json:"orderNo"`
CouponNo string `json:"couponNo,omitempty"`
CouponCode string `json:"couponCode,omitempty"`
Status int `json:"status"`
ExpireTime string `json:"expireTime,omitempty"`
}
// ===== 接口2卡券/直充/微信立减金订单查询接口 =====
// QueryRequest 查询请求业务参数
type QueryRequest struct {
ActCode string `json:"actCode"`
OrderNo string `json:"orderNo"`
}
// QueryResponse 查询响应
type QueryResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *QueryData `json:"data"`
}
// QueryData 查询响应解密后数据
type QueryData struct {
OrderNo string `json:"orderNo"`
Status int `json:"status"`
Account string `json:"account,omitempty"`
CardInfo *CardInfo `json:"cardInfo,omitempty"`
CouponId string `json:"couponId,omitempty"`
}
// CardInfo 卡券信息
type CardInfo struct {
CouponNo string `json:"couponNo"`
CouponCode string `json:"couponCode"`
ExpireTime string `json:"expireTime"`
}
// ===== 接口3微信立减金订单充值接口 =====
// WechatRechargeRequest 微信立减金充值请求业务参数
type WechatRechargeRequest struct {
ActCode string `json:"actCode"`
OrderNo string `json:"orderNo"`
GoodsCode string `json:"goodsCode"`
ActOrderNum string `json:"actOrderNum"`
AppId string `json:"appId"`
OpenId string `json:"openId"`
CallbackUrl string `json:"callbackUrl,omitempty"`
}
// WechatRechargeResponse 微信立减金充值响应
type WechatRechargeResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *WechatRechargeData `json:"data"`
}
// WechatRechargeData 微信立减金充值响应解密后数据
type WechatRechargeData struct {
OrderNo string `json:"orderNo"`
Status int `json:"status"`
CouponId string `json:"couponId,omitempty"`
}
// ===== 接口4回调通知接口 =====
// CallbackRequest 回调通知请求业务参数
type CallbackRequest 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"`
}
// ===== 通用结构 =====
// EncryptedRequest 加密请求体
type EncryptedRequest struct {
EncryptedData string `json:"encryptedData"`
}
// RawResponse 原始响应结构data字段为加密字符串
type RawResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data string `json:"data"` // 加密后的JSON字符串
}
```
Now let me write the crypto.go file with the SM4 and SM3 implementations adapted for this SDK.
For the SM3 with salt, the Java code `SmUtil.sm3WithSalt(sm3Salt.getBytes()).digestHex(timestamp+encryptedData)` is essentially HMAC-SM3 where the salt is the key. So I'll use HMAC-SM3.
For SM4-CBC encryption, the encrypted data includes IV prepended to the ciphertext, encoded in base64.
Let me write all files now.
Actually, looking at the response format more carefully:
```json
{
"code": 0,
"msg": "请求成功",
"data": "{\"orderNo\":\"HM17575805323790000127073398f3772c00\",...}"
}
```
The `data` field is a JSON string (the encrypted data). So when we receive the response, we first parse the outer JSON to get `code`, `msg`, and `data` (as string), then decrypt `data` to get the inner JSON.
Let me structure this properly.
Now let me write all 6 files:
1. go.mod
2. errors.go
3. types.go
4. crypto.go
5. client.go
6. example_test.go
Let me write them all 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"
// Error 自定义错误类型,包含返回码和错误信息
type Error struct {
Code int `json:"code"`
Message string `json:"msg"`
}
// Error 实现 error 接口
func (e *Error) Error() string {
return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Message)
}
// IsSuccess 判断业务返回码是否表示成功
func IsSuccess(code int) bool {
return code == 0
}
```
// File: xy_sh/types.go
```go
package xy_sh
// ===== 接口1卡券/直充权益下单接口 =====
// OrderRequest 下单接口业务请求参数
type OrderRequest struct {
ActCode string `json:"actCode"` // 活动code
GoodsCode string `json:"goodsCode"` // 供应商商品编号
ActOrderNum string `json:"actOrderNum"` // 活动方订单号,唯一
Account string `json:"account,omitempty"` // 充值账号
CallbackUrl string `json:"callbackUrl,omitempty"` // 回调地址
}
// OrderData 下单接口响应解密后的业务数据
type OrderData 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"` // 有效期 yyyy-MM-dd HH:mm:ss
}
// ===== 接口2卡券/直充/微信立减金订单查询接口 =====
// QueryRequest 查询接口业务请求参数
type QueryRequest struct {
ActCode string `json:"actCode"` // 活动code
OrderNo string `json:"orderNo"` // 供应商订单号
}
// QueryData 查询接口响应解密后的业务数据
type QueryData 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
}
// CardInfo 卡券信息
type CardInfo struct {
CouponNo string `json:"couponNo"` // 卡号/短链
CouponCode string `json:"couponCode"` // 卡密
ExpireTime string `json:"expireTime"` // 过期时间 yyyy-MM-dd HH:mm:ss
}
// ===== 接口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"` // 回调地址
}
// WechatRechargeData 微信立减金充值响应解密后的业务数据
type WechatRechargeData struct {
OrderNo string `json:"orderNo"` // 供应商订单号
Status int `json:"status"` // 订单状态
CouponId string `json:"couponId,omitempty"` // 微信优惠id
}
// ===== 接口4回调通知接口 =====
// CallbackRequest 回调通知业务请求参数
type CallbackRequest 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
}
// ===== 通用结构 =====
// EncryptedRequest 加密请求体
type EncryptedRequest struct {
EncryptedData string `json:"encryptedData"`
}
// RawResponse 原始响应结构data 字段为加密后的 JSON 字符串
type RawResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data string `json:"data"`
}
```
// File: xy_sh/crypto.go
```go
package xy_sh
import (
"bytes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"time"
"github.com/tjfoc/gmsm/sm3"
"github.com/tjfoc/gmsm/sm4"
)
// GenerateTimestampMillis 生成毫秒级时间戳字符串
func GenerateTimestampMillis() string {
return fmt.Sprintf("%d", time.Now().UnixMilli())
}
// SM4Encrypt SM4-CBC模式加密
// 使用随机IVIV拼在密文前面最终输出base64编码字符串
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编码字符串前16字节为IV后续为密文
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
}
// GenerateSign 生成签名
// 签名规则将毫秒级时间戳timestamp字符串与encryptedData字符串直接拼接
// 使用HMAC-SM3算法结合预先约定的SM3 salt进行加密输出十六进制字符串
func GenerateSign(timestamp string, encryptedData string, sm3Salt []byte) string {
data := timestamp + encryptedData
mac := hmac.New(sm3.New, sm3Salt)
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(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 供应商接口客户端
type Client struct {
httpClient *http.Client
sm4Key []byte // SM4加密密钥16字节
sm3Salt []byte // SM3签名盐值
orderURL string // 下单接口地址
queryURL string // 查询接口地址
rechargeURL string // 微信立减金充值接口地址
}
// NewClient 创建新的客户端
// - sm4Key: SM4加密密钥必须为16字节
// - sm3Salt: SM3签名盐值
// - orderURL: 下单接口地址
// - queryURL: 查询接口地址
// - rechargeURL: 微信立减金充值接口地址
func NewClient(sm4Key []byte, sm3Salt []byte, orderURL, queryURL, rechargeURL string) (*Client, error) {
if len(sm4Key) != 16 {
return nil, fmt.Errorf("SM4密钥长度必须为16字节")
}
if len(sm3Salt) == 0 {
return nil, fmt.Errorf("SM3盐值不能为空")
}
return &Client{
httpClient: &http.Client{},
sm4Key: sm4Key,
sm3Salt: sm3Salt,
orderURL: orderURL,
queryURL: queryURL,
rechargeURL: rechargeURL,
}, nil
}
// SetHTTPClient 设置自定义HTTP客户端
func (c *Client) SetHTTPClient(httpClient *http.Client) {
c.httpClient = httpClient
}
// doRequest 执行加密请求并解密响应
func (c *Client) doRequest(ctx context.Context, url string, bizReq interface{}) (*RawResponse, error) {
// 1. 序列化业务参数为JSON
bizBody, err := json.Marshal(bizReq)
if err != nil {
return nil, fmt.Errorf("序列化业务参数失败: %v", err)
}
// 2. SM4加密业务参数
encryptedData, err := SM4Encrypt(bizBody, c.sm4Key)
if err != nil {
return nil, fmt.Errorf("SM4加密失败: %v", err)
}
// 3. 生成时间戳和签名
timestamp := GenerateTimestampMillis()
sign := GenerateSign(timestamp, encryptedData, c.sm3Salt)
// 4. 构建加密请求体
reqBody := EncryptedRequest{EncryptedData: encryptedData}
reqBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %v", err)
}
// 5. 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBytes))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("timestamp", timestamp)
req.Header.Set("sign", sign)
// 6. 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 7. 读取响应体
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %v", err)
}
// 8. 解析原始响应
var rawResp RawResponse
if err := json.Unmarshal(respBytes, &rawResp); err != nil {
return nil, fmt.Errorf("解析响应JSON失败: %v", err)
}
return &rawResp, nil
}
// decryptData 解密响应中的data字段
func (c *Client) decryptData(encryptedData string, target interface{}) error {
if encryptedData == "" {
return nil
}
plaintext, err := SM4Decrypt(encryptedData, c.sm4Key)
if err != nil {
return fmt.Errorf("SM4解密失败: %v", err)
}
if err := json.Unmarshal(plaintext, target); err != nil {
return fmt.Errorf("解析解密后数据失败: %v", err)
}
return nil
}
// Order 卡券/直充权益下单
// 适用于卡密、直充商品下单
func (c *Client) Order(ctx context.Context, req *OrderRequest) (*OrderResponse, error) {
rawResp, err := c.doRequest(ctx, c.orderURL, req)
if err != nil {
return nil, err
}
resp := &OrderResponse{
Code: rawResp.Code,
Msg: rawResp.Msg,
}
if rawResp.Data != "" {
var data OrderData
if err := c.decryptData(rawResp.Data, &data); err != nil {
return nil, err
}
resp.Data = &data
}
return resp, nil
}
// QueryOrder 卡券/直充/微信立减金订单查询
// 通过供应商订单号查询订单状态
func (c *Client) QueryOrder(ctx context.Context, req *QueryRequest) (*QueryResponse, error) {
rawResp, err := c.doRequest(ctx, c.queryURL, req)
if err != nil {
return nil, err
}
resp := &QueryResponse{
Code: rawResp.Code,
Msg: rawResp.Msg,
}
if rawResp.Data != "" {
var data QueryData
if err := c.decryptData(rawResp.Data, &data); err != nil {
return nil, err
}
resp.Data = &data
}
return resp, nil
}
// WechatRecharge 微信立减金订单充值
func (c *Client) WechatRecharge(ctx context.Context, req *WechatRechargeRequest) (*WechatRechargeResponse, error) {
rawResp, err := c.doRequest(ctx, c.rechargeURL, req)
if err != nil {
return nil, err
}
resp := &WechatRechargeResponse{
Code: rawResp.Code,
Msg: rawResp.Msg,
}
if rawResp.Data != "" {
var data WechatRechargeData
if err := c.decryptData(rawResp.Data, &data); err != nil {
return nil, err
}
resp.Data = &data
}
return resp, nil
}
// SendCallback 发送回调通知(供应商调用行方回调接口)
// 返回的 CallbackResponse 中 StatusCode 为 HTTP 状态码Body 为响应体内容
func (c *Client) SendCallback(ctx context.Context, callbackURL string, req *CallbackRequest) (*CallbackResponse, error) {
// 1. 序列化业务参数
bizBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("序列化回调参数失败: %v", err)
}
// 2. SM4加密
encryptedData, err := SM4Encrypt(bizBody, c.sm4Key)
if err != nil {
return nil, fmt.Errorf("SM4加密失败: %v", err)
}
// 3. 生成时间戳和签名
timestamp := GenerateTimestampMillis()
sign := GenerateSign(timestamp, encryptedData, c.sm3Salt)
// 4. 构建请求体
reqBody := EncryptedRequest{EncryptedData: encryptedData}
reqBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %v", err)
}
// 5. 创建HTTP请求
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, callbackURL, bytes.NewReader(reqBytes))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("timestamp", timestamp)
httpReq.Header.Set("sign", sign)
// 6. 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("发送回调请求失败: %v", err)
}
defer resp.Body.Close()
// 7. 读取响应体
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取回调响应失败: %v", err)
}
return &CallbackResponse{
StatusCode: resp.StatusCode,
Body: string(respBytes),
}, nil
}
```
// File: xy_sh/example_test.go
```go
package xy_sh
import (
"context"
"fmt"
"log"
)
func ExampleClient_Order() {
// 初始化客户端
sm4Key := []byte("1234567890abcdef") // 16字节SM4密钥
sm3Salt := []byte("my_sm3_salt") // SM3盐值
orderURL := "https://api.example.com/order"
queryURL := "https://api.example.com/query"
rechargeURL := "https://api.example.com/recharge"
client, err := NewClient(sm4Key, sm3Salt, orderURL, queryURL, rechargeURL)
if err != nil {
log.Fatal(err)
}
// 下单
req := &OrderRequest{
ActCode: "ACT001",
GoodsCode: "123456",
ActOrderNum: "00001",
Account: "19912345678",
CallbackUrl: "https://xxx/notice",
}
resp, err := client.Order(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fmt.Printf("code: %d, msg: %s\n", resp.Code, resp.Msg)
if resp.Data != nil {
fmt.Printf("orderNo: %s, status: %d\n", resp.Data.OrderNo, resp.Data.Status)
}
}
func ExampleClient_QueryOrder() {
sm4Key := []byte("1234567890abcdef")
sm3Salt := []byte("my_sm3_salt")
client, err := NewClient(sm4Key, sm3Salt, "https://api.example.com/order", "https://api.example.com/query", "https://api.example.com/recharge")
if err != nil {
log.Fatal(err)
}
req := &QueryRequest{
ActCode: "FBG6vdYqGE4mGX7EH/woEg==",
OrderNo: "HM1757046717684000102707339840b23222",
}
resp, err := client.QueryOrder(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fmt.Printf("code: %d, msg: %s\n", resp.Code, resp.Msg)
if resp.Data != nil {
fmt.Printf("orderNo: %s, status: %d\n", resp.Data.OrderNo, resp.Data.Status)
}
}
func ExampleClient_WechatRecharge() {
sm4Key := []byte("1234567890abcdef")
sm3Salt := []byte("my_sm3_salt")
client, err := NewClient(sm4Key, sm3Salt, "https://api.example.com/order", "https://api.example.com/query", "https://api.example.com/recharge")
if err != nil {
log.Fatal(err)
}
req := &WechatRechargeRequest{
ActCode: "FBG6vdYqGE4mGX7EH/woEg==",
GoodsCode: "0001",
ActOrderNum: "0001",
OpenId: "0001",
AppId: "00001",
}
resp, err := client.WechatRecharge(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fmt.Printf("code: %d, msg: %s\n", resp.Code, resp.Msg)
if resp.Data != nil {
fmt.Printf("orderNo: %s, status: %d, couponId: %s\n", resp.Data.OrderNo, resp.Data.Status, resp.Data.CouponId)
}
}
func ExampleClient_SendCallback() {
sm4Key := []byte("1234567890abcdef")
sm3Salt := []byte("my_sm3_salt")
client, err := NewClient(sm4Key, sm3Salt, "https://api.example.com/order", "https://api.example.com/query", "https://api.example.com/recharge")
if err != nil {
log.Fatal(err)
}
req := &CallbackRequest{
OrderNo: "HM202509291010001",
ActOrderNum: "XY2025092910100001",
Status: 3,
Account: "19912345678",
}
resp, err := client.SendCallback(context.Background(), "https://bank.example.com/callback", req)
if err != nil {
log.Fatal(err)
}
fmt.Printf("statusCode: %d, body: %s\n", resp.StatusCode, resp.Body)
}
```
=== 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"
}