添加文件: xy-shanghai/generate.md
This commit is contained in:
parent
1d95dba971
commit
7343bec283
|
|
@ -0,0 +1,500 @@
|
||||||
|
// File: xy-shanghai/go.mod
|
||||||
|
```go
|
||||||
|
module xy-shanghai
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/tjfoc/gmsm v1.4.1
|
||||||
|
```
|
||||||
|
|
||||||
|
// File: xy-shanghai/client.go
|
||||||
|
```go
|
||||||
|
package xy-shanghai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 是 SDK 的客户端,用于调用供应商的 API 接口。
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
sm4Key []byte
|
||||||
|
sm3Salt []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 定义客户端配置选项。
|
||||||
|
type Option func(*Client)
|
||||||
|
|
||||||
|
// NewClient 创建一个新的 Client 实例。
|
||||||
|
// 必须通过 WithSM4Key 和 WithSM3Salt 设置密钥和盐值。
|
||||||
|
func NewClient(opts ...Option) *Client {
|
||||||
|
c := &Client{
|
||||||
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBaseURL 设置基础 URL。
|
||||||
|
func WithBaseURL(baseURL string) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.baseURL = baseURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeout 设置 HTTP 请求超时时间。
|
||||||
|
func WithTimeout(timeout time.Duration) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.httpClient.Timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPClient 设置自定义 HTTP 客户端。
|
||||||
|
func WithHTTPClient(httpClient *http.Client) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.httpClient = httpClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSM4Key 设置 SM4 加密密钥(16 字节)。
|
||||||
|
func WithSM4Key(key []byte) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.sm4Key = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSM3Salt 设置 SM3 签名盐值。
|
||||||
|
func WithSM3Salt(salt []byte) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.sm3Salt = salt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequest 发送 POST 请求,自动加密业务数据并签名。
|
||||||
|
func (c *Client) doRequest(ctx context.Context, path string, bizData interface{}) (*CommonResponse, error) {
|
||||||
|
if c.baseURL == "" {
|
||||||
|
return nil, ErrBaseURLNotSet
|
||||||
|
}
|
||||||
|
if len(c.sm4Key) == 0 {
|
||||||
|
return nil, ErrSM4KeyNotSet
|
||||||
|
}
|
||||||
|
if len(c.sm3Salt) == 0 {
|
||||||
|
return nil, ErrSM3SaltNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化业务数据
|
||||||
|
bizJSON, err := json.Marshal(bizData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal biz data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SM4 加密
|
||||||
|
encryptedData, err := SM4Encrypt(c.sm4Key, bizJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sm4 encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成时间戳(毫秒)
|
||||||
|
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||||
|
|
||||||
|
// 计算签名
|
||||||
|
sign := SM3WithSalt(c.sm3Salt, timestamp+string(encryptedData))
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
reqBody := &EncryptedRequest{
|
||||||
|
EncryptedData: string(encryptedData),
|
||||||
|
}
|
||||||
|
reqJSON, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HTTP 请求
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(reqJSON))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("timestamp", timestamp)
|
||||||
|
req.Header.Set("sign", sign)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("http do: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析公共响应
|
||||||
|
var commonResp CommonResponse
|
||||||
|
if err := json.Unmarshal(body, &commonResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal common response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 data 不为空,解密 data 字段
|
||||||
|
if commonResp.Data != "" {
|
||||||
|
decrypted, err := SM4Decrypt(c.sm4Key, []byte(commonResp.Data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sm4 decrypt response data: %w", err)
|
||||||
|
}
|
||||||
|
commonResp.Data = string(decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &commonResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrder 卡券/直充权益下单接口。
|
||||||
|
func (c *Client) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CommonResponse, error) {
|
||||||
|
return c.doRequest(ctx, "/createOrder", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrder 卡券/直充/微信立减金订单查询接口。
|
||||||
|
func (c *Client) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*CommonResponse, error) {
|
||||||
|
return c.doRequest(ctx, "/queryOrder", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RechargeOrder 微信立减金订单充值接口。
|
||||||
|
func (c *Client) RechargeOrder(ctx context.Context, req *RechargeOrderRequest) (*CommonResponse, error) {
|
||||||
|
return c.doRequest(ctx, "/rechargeOrder", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCallbackRequest 解析回调请求,验证签名并解密业务数据。
|
||||||
|
// 返回解密后的业务数据(JSON 字符串)和错误。
|
||||||
|
func (c *Client) ParseCallbackRequest(timestamp, sign string, encryptedData string) (string, error) {
|
||||||
|
if len(c.sm3Salt) == 0 {
|
||||||
|
return "", ErrSM3SaltNotSet
|
||||||
|
}
|
||||||
|
if len(c.sm4Key) == 0 {
|
||||||
|
return "", ErrSM4KeyNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
expectedSign := SM3WithSalt(c.sm3Salt, timestamp+encryptedData)
|
||||||
|
if sign != expectedSign {
|
||||||
|
return "", ErrInvalidSign
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密
|
||||||
|
decrypted, err := SM4Decrypt(c.sm4Key, []byte(encryptedData))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("sm4 decrypt callback data: %w", err)
|
||||||
|
}
|
||||||
|
return string(decrypted), nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
// File: xy-shanghai/types.go
|
||||||
|
```go
|
||||||
|
package xy-shanghai
|
||||||
|
|
||||||
|
// EncryptedRequest 加密请求体。
|
||||||
|
type EncryptedRequest struct {
|
||||||
|
EncryptedData string `json:"encryptedData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonResponse 公共响应结构体。
|
||||||
|
type CommonResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data string `json:"data"` // 加密的 JSON 字符串,解密后使用
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrderRequest 卡券/直充权益下单接口的业务数据。
|
||||||
|
type CreateOrderRequest struct {
|
||||||
|
ActCode string `json:"actCode"`
|
||||||
|
GoodsCode string `json:"goodsCode"`
|
||||||
|
ActOrderNum string `json:"actOrderNum"`
|
||||||
|
Account string `json:"account,omitempty"`
|
||||||
|
CallbackURL string `json:"callbackUrl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrderResponseData 下单接口响应解密后的 data 字段。
|
||||||
|
type CreateOrderResponseData struct {
|
||||||
|
OrderNo string `json:"orderNo"`
|
||||||
|
CouponNo string `json:"couponNo,omitempty"`
|
||||||
|
CouponCode string `json:"couponCode,omitempty"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
ExpireTime string `json:"expireTime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrderRequest 订单查询接口的业务数据。
|
||||||
|
type QueryOrderRequest struct {
|
||||||
|
ActCode string `json:"actCode"`
|
||||||
|
OrderNo string `json:"orderNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrderResponseData 订单查询接口响应解密后的 data 字段。
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardInfo 卡券信息。
|
||||||
|
type CardInfo struct {
|
||||||
|
CouponNo string `json:"couponNo"`
|
||||||
|
CouponCode string `json:"couponCode"`
|
||||||
|
ExpireTime string `json:"expireTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RechargeOrderRequest 微信立减金充值接口的业务数据。
|
||||||
|
type RechargeOrderRequest 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RechargeOrderResponseData 充值接口响应解密后的 data 字段。
|
||||||
|
type RechargeOrderResponseData struct {
|
||||||
|
OrderNo string `json:"orderNo"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
CouponID string `json:"couponId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallbackBizData 回调通知的业务数据(加密前)。
|
||||||
|
type CallbackBizData 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"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
// File: xy-shanghai/crypto.go
|
||||||
|
```go
|
||||||
|
package xy-shanghai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tjfoc/gmsm/sm3"
|
||||||
|
"github.com/tjfoc/gmsm/sm4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SM4Encrypt 使用 SM4 算法加密数据(ECB 模式,PKCS7 填充)。
|
||||||
|
// key 长度必须为 16 字节。
|
||||||
|
func SM4Encrypt(key, plaintext []byte) ([]byte, error) {
|
||||||
|
if len(key) != 16 {
|
||||||
|
return nil, fmt.Errorf("sm4 key must be 16 bytes, got %d", len(key))
|
||||||
|
}
|
||||||
|
ciphertext, err := sm4.Sm4Ecb(key, plaintext, true) // true 表示加密
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sm4 ecb encrypt: %w", err)
|
||||||
|
}
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SM4Decrypt 使用 SM4 算法解密数据(ECB 模式,PKCS7 填充)。
|
||||||
|
func SM4Decrypt(key, ciphertext []byte) ([]byte, error) {
|
||||||
|
if len(key) != 16 {
|
||||||
|
return nil, fmt.Errorf("sm4 key must be 16 bytes, got %d", len(key))
|
||||||
|
}
|
||||||
|
plaintext, err := sm4.Sm4Ecb(key, ciphertext, false) // false 表示解密
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sm4 ecb decrypt: %w", err)
|
||||||
|
}
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SM3WithSalt 使用 SM3 算法加盐计算哈希。
|
||||||
|
// salt 为盐值,data 为待签名字符串。
|
||||||
|
func SM3WithSalt(salt []byte, data string) string {
|
||||||
|
h := sm3.New()
|
||||||
|
h.Write(salt)
|
||||||
|
h.Write([]byte(data))
|
||||||
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSM4Key 生成一个随机的 16 字节 SM4 密钥。
|
||||||
|
func GenerateSM4Key() ([]byte, error) {
|
||||||
|
key := make([]byte, 16)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate random key: %w", err)
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
// File: xy-shanghai/errors.go
|
||||||
|
```go
|
||||||
|
package xy-shanghai
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrBaseURLNotSet 表示未设置基础 URL。
|
||||||
|
ErrBaseURLNotSet = errors.New("base URL is not set")
|
||||||
|
// ErrSM4KeyNotSet 表示未设置 SM4 密钥。
|
||||||
|
ErrSM4KeyNotSet = errors.New("SM4 key is not set")
|
||||||
|
// ErrSM3SaltNotSet 表示未设置 SM3 盐值。
|
||||||
|
ErrSM3SaltNotSet = errors.New("SM3 salt is not set")
|
||||||
|
// ErrInvalidSign 表示签名验证失败。
|
||||||
|
ErrInvalidSign = errors.New("invalid sign")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
// File: xy-shanghai/api_key.go
|
||||||
|
```go
|
||||||
|
package xy-shanghai
|
||||||
|
|
||||||
|
// APIKey 用于管理活动 code 和对应的密钥信息。
|
||||||
|
// 根据文档,actCode 可约定为各供应商的项目编号和密钥的拼接加密字符串。
|
||||||
|
// 此结构体用于存储解密后的信息,实际使用时可根据需要扩展。
|
||||||
|
type APIKey struct {
|
||||||
|
ProjectID string // 项目编号
|
||||||
|
SecretKey string // 密钥
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
// File: xy-shanghai/example_test.go
|
||||||
|
```go
|
||||||
|
package xy-shanghai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 测试用的密钥和盐值(仅用于测试,生产环境需安全保管)
|
||||||
|
var (
|
||||||
|
testSM4Key = []byte("1234567890abcdef") // 16 字节
|
||||||
|
testSM3Salt = []byte("test-salt")
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestClient() *Client {
|
||||||
|
return NewClient(
|
||||||
|
WithBaseURL("http://localhost:8080"),
|
||||||
|
WithTimeout(5*time.Second),
|
||||||
|
WithSM4Key(testSM4Key),
|
||||||
|
WithSM3Salt(testSM3Salt),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateOrder 测试卡券/直充权益下单接口。
|
||||||
|
func TestCreateOrder(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
req := &CreateOrderRequest{
|
||||||
|
ActCode: "ACT001",
|
||||||
|
GoodsCode: "123456",
|
||||||
|
ActOrderNum: "00001",
|
||||||
|
Account: "19912345678",
|
||||||
|
CallbackURL: "https://example.com/notice",
|
||||||
|
}
|
||||||
|
resp, err := client.CreateOrder(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateOrder failed: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Code != 0 {
|
||||||
|
t.Fatalf("unexpected code: %d, msg: %s", resp.Code, resp.Msg)
|
||||||
|
}
|
||||||
|
// 解密 data 并解析
|
||||||
|
var data CreateOrderResponseData
|
||||||
|
if err := json.Unmarshal([]byte(resp.Data), &data); err != nil {
|
||||||
|
t.Fatalf("unmarshal response data: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("OrderNo: %s, Status: %d\n", data.OrderNo, data.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestQueryOrder 测试订单查询接口。
|
||||||
|
func TestQueryOrder(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
req := &QueryOrderRequest{
|
||||||
|
ActCode: "FBG6vdYqGE4mGX7EH/woEg==",
|
||||||
|
OrderNo: "HM1757046717684000102707339840b23222",
|
||||||
|
}
|
||||||
|
resp, err := client.QueryOrder(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("QueryOrder failed: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Code != 0 {
|
||||||
|
t.Fatalf("unexpected code: %d, msg: %s", resp.Code, resp.Msg)
|
||||||
|
}
|
||||||
|
var data QueryOrderResponseData
|
||||||
|
if err := json.Unmarshal([]byte(resp.Data), &data); err != nil {
|
||||||
|
t.Fatalf("unmarshal response data: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("OrderNo: %s, Status: %d\n", data.OrderNo, data.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRechargeOrder 测试微信立减金充值接口。
|
||||||
|
func TestRechargeOrder(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
req := &RechargeOrderRequest{
|
||||||
|
ActCode: "FBG6vdYqGE4mGX7EH/woEg==",
|
||||||
|
GoodsCode: "0001",
|
||||||
|
ActOrderNum: "0001",
|
||||||
|
OpenID: "0001",
|
||||||
|
AppID: "00001",
|
||||||
|
}
|
||||||
|
resp, err := client.RechargeOrder(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RechargeOrder failed: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Code != 0 {
|
||||||
|
t.Fatalf("unexpected code: %d, msg: %s", resp.Code, resp.Msg)
|
||||||
|
}
|
||||||
|
var data RechargeOrderResponseData
|
||||||
|
if err := json.Unmarshal([]byte(resp.Data), &data); err != nil {
|
||||||
|
t.Fatalf("unmarshal response data: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("OrderNo: %s, Status: %d, CouponID: %s\n", data.OrderNo, data.Status, data.CouponID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseCallbackRequest 测试回调请求解析。
|
||||||
|
func TestParseCallbackRequest(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
// 模拟回调请求参数
|
||||||
|
bizData := CallbackBizData{
|
||||||
|
OrderNo: "HM202509291010001",
|
||||||
|
ActOrderNum: "XY2025092910100001",
|
||||||
|
Status: 3,
|
||||||
|
Account: "19912345678",
|
||||||
|
}
|
||||||
|
bizJSON, _ := json.Marshal(bizData)
|
||||||
|
encrypted, _ := SM4Encrypt(testSM4Key, bizJSON)
|
||||||
|
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||||
|
sign := SM3WithSalt(testSM3Salt, timestamp+string(encrypted))
|
||||||
|
|
||||||
|
// 解析
|
||||||
|
decrypted, err := client.ParseCallbackRequest(timestamp, sign, string(encrypted))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseCallbackRequest failed: %v", err)
|
||||||
|
}
|
||||||
|
var parsed CallbackBizData
|
||||||
|
if err := json.Unmarshal([]byte(decrypted), &parsed); err != nil {
|
||||||
|
t.Fatalf("unmarshal callback data: %v", err)
|
||||||
|
}
|
||||||
|
if parsed.OrderNo != "HM202509291010001" {
|
||||||
|
t.Fatalf("unexpected orderNo: %s", parsed.OrderNo)
|
||||||
|
}
|
||||||
|
fmt.Printf("Callback parsed successfully: %+v\n", parsed)
|
||||||
|
}
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue