From 7343bec2832019f82a1ae2769950b1f11104dfcf Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Tue, 21 Jul 2026 17:23:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20xy-shan?= =?UTF-8?q?ghai/generate.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xy-shanghai/generate.md | 500 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 xy-shanghai/generate.md diff --git a/xy-shanghai/generate.md b/xy-shanghai/generate.md new file mode 100644 index 0000000..adb2c0f --- /dev/null +++ b/xy-shanghai/generate.md @@ -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) +} +``` \ No newline at end of file