xy_sh-20260724174214/xy_sh/valid.md

636 lines
17 KiB
Markdown
Raw Permalink 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.

// 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)
}
```