xy-shanghai-20260721-171915/xy-shanghai/valid.md

500 lines
13 KiB
Markdown
Raw 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-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 xyshanghai
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 xyshanghai
// 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 xyshanghai
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 xyshanghai
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 xyshanghai
// APIKey 用于管理活动 code 和对应的密钥信息。
// 根据文档actCode 可约定为各供应商的项目编号和密钥的拼接加密字符串。
// 此结构体用于存储解密后的信息,实际使用时可根据需要扩展。
type APIKey struct {
ProjectID string // 项目编号
SecretKey string // 密钥
}
```
// File: xy-shanghai/example_test.go
```go
package xyshanghai
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)
}
```