添加文件: xy_sh/valid.md

This commit is contained in:
renzhiyuan 2026-07-24 17:42:19 +08:00
parent 70eefb0d8b
commit e1c7d7283d
1 changed files with 636 additions and 0 deletions

636
xy_sh/valid.md Normal file
View File

@ -0,0 +1,636 @@
// 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
}
// OrderResponse 下单接口响应
type OrderResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *OrderData `json:"data,omitempty"`
}
// ===== 接口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
}
// QueryResponse 查询接口响应
type QueryResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *QueryData `json:"data,omitempty"`
}
// 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
}
// WechatRechargeResponse 微信立减金充值响应
type WechatRechargeResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *WechatRechargeData `json:"data,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"` // 微信优惠id
}
// CallbackResponse 回调通知响应HTTP响应
type CallbackResponse struct {
StatusCode int `json:"-"` // HTTP状态码
Body string `json:"-"` // 响应体内容
}
// ===== 通用结构 =====
// 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/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/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)
}
```