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