From 086c36e321bdaf10a90e8f82855b13775f6717e3 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Tue, 21 Jul 2026 18:16:26 +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 | 668 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 668 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..6f50fd0 --- /dev/null +++ b/xy-shanghai/generate.md @@ -0,0 +1,668 @@ +// File: xy-shanghai/go.mod +```go +module xy-shanghai + +go 1.21 + +require github.com/tjfoc/gmsm v1.4.1 +``` + +// File: xy-shanghai/errors.go +```go +package xy-shanghai + +import "fmt" + +// ErrorCode 定义错误码类型 +type ErrorCode int + +const ( + // ErrCodeSuccess 成功 + ErrCodeSuccess ErrorCode = 0 + // ErrCodeFailure 失败 + ErrCodeFailure ErrorCode = -1 +) + +// SDKError 自定义SDK错误 +type SDKError struct { + Code ErrorCode + Message string + Err error +} + +func (e *SDKError) Error() string { + if e.Err != nil { + return fmt.Sprintf("code=%d, message=%s, err=%v", e.Code, e.Message, e.Err) + } + return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message) +} + +// Unwrap 返回内部错误 +func (e *SDKError) Unwrap() error { + return e.Err +} + +// NewSDKError 创建SDK错误 +func NewSDKError(code ErrorCode, message string, err error) *SDKError { + return &SDKError{ + Code: code, + Message: message, + Err: err, + } +} +``` + +// File: xy-shanghai/types.go +```go +package xy-shanghai + +// CommonRequestHeader 通用请求头 +type CommonRequestHeader struct { + Timestamp string `json:"timestamp"` + Sign string `json:"sign"` +} + +// CommonRequestBody 通用请求体 +type CommonRequestBody struct { + EncryptedData string `json:"encryptedData"` +} + +// CommonResponse 公共响应 +type CommonResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data *string `json:"data,omitempty"` // SM4加密后的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"` +} + +// CreateOrderResponse 下单响应业务数据(解密后) +type CreateOrderResponse 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"` +} + +// QueryOrderResponse 订单查询响应业务数据(解密后) +type QueryOrderResponse 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"` +} + +// RechargeOrderResponse 充值响应业务数据(解密后) +type RechargeOrderResponse struct { + OrderNo string `json:"orderNo"` + Status int `json:"status"` + CouponID string `json:"couponId"` +} + +// 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"` +} +``` + +// File: xy-shanghai/crypto.go +```go +package xy-shanghai + +import ( + "crypto/cipher" + "encoding/base64" + "encoding/hex" + "fmt" + + "github.com/tjfoc/gmsm/sm3" + "github.com/tjfoc/gmsm/sm4" +) + +// SM4Encrypt 使用SM4 ECB模式加密数据,返回base64编码字符串 +func SM4Encrypt(key, plaintext []byte) (string, error) { + block, err := sm4.NewCipher(key) + if err != nil { + return "", fmt.Errorf("sm4 new cipher: %w", err) + } + + // PKCS7填充 + plaintext = pkcs7Padding(plaintext, block.BlockSize()) + + // ECB模式 + mode := newECBEncrypter(block) + ciphertext := make([]byte, len(plaintext)) + mode.CryptBlocks(ciphertext, plaintext) + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// SM4Decrypt 解密base64编码的SM4 ECB密文,返回明文 +func SM4Decrypt(key []byte, ciphertextBase64 string) ([]byte, error) { + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64) + if err != nil { + return nil, fmt.Errorf("base64 decode: %w", err) + } + + block, err := sm4.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("sm4 new cipher: %w", err) + } + + mode := newECBDecrypter(block) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + // 去除PKCS7填充 + plaintext, err = pkcs7Unpadding(plaintext, block.BlockSize()) + if err != nil { + return nil, fmt.Errorf("pkcs7 unpadding: %w", err) + } + + return plaintext, nil +} + +// SM3Sign 使用SM3加盐签名,返回十六进制字符串 +func SM3Sign(salt []byte, data string) string { + h := sm3.Sm3WithSalt(salt) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +// pkcs7Padding PKCS7填充 +func pkcs7Padding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + padtext := make([]byte, padding) + for i := range padtext { + padtext[i] = byte(padding) + } + return append(src, padtext...) +} + +// pkcs7Unpadding 去除PKCS7填充 +func pkcs7Unpadding(src []byte, blockSize int) ([]byte, error) { + length := len(src) + if length == 0 { + return nil, fmt.Errorf("empty data") + } + unpadding := int(src[length-1]) + if unpadding > blockSize || unpadding == 0 { + return nil, fmt.Errorf("invalid padding") + } + for i := length - unpadding; i < length; i++ { + if src[i] != byte(unpadding) { + return nil, fmt.Errorf("invalid padding") + } + } + return src[:length-unpadding], nil +} + +// ecb 实现ECB模式(标准库未提供) +type ecb struct { + b cipher.Block + blockSize int +} + +func newECB(b cipher.Block) *ecb { + return &ecb{ + b: b, + blockSize: b.BlockSize(), + } +} + +type ecbEncrypter ecb + +func newECBEncrypter(b cipher.Block) cipher.BlockMode { + return (*ecbEncrypter)(newECB(b)) +} + +func (x *ecbEncrypter) BlockSize() int { return x.blockSize } + +func (x *ecbEncrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for i := 0; i < len(src); i += x.blockSize { + x.b.Encrypt(dst[i:i+x.blockSize], src[i:i+x.blockSize]) + } +} + +type ecbDecrypter ecb + +func newECBDecrypter(b cipher.Block) cipher.BlockMode { + return (*ecbDecrypter)(newECB(b)) +} + +func (x *ecbDecrypter) BlockSize() int { return x.blockSize } + +func (x *ecbDecrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for i := 0; i < len(src); i += x.blockSize { + x.b.Decrypt(dst[i:i+x.blockSize], src[i:i+x.blockSize]) + } +} +``` + +// File: xy-shanghai/api_key.go +```go +package xy-shanghai + +// APIKey 存储SM4密钥和SM3盐值 +type APIKey struct { + SM4Key []byte + SM3Salt []byte +} + +// NewAPIKey 创建APIKey实例 +func NewAPIKey(sm4Key, sm3Salt []byte) *APIKey { + return &APIKey{ + SM4Key: sm4Key, + SM3Salt: sm3Salt, + } +} +``` + +// File: xy-shanghai/client.go +```go +package xy-shanghai + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "time" +) + +// Client 供应商API客户端 +type Client struct { + baseURL string + httpClient *http.Client + apiKey *APIKey +} + +// Option 客户端配置选项 +type Option func(*Client) + +// 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 + } +} + +// NewClient 创建新的客户端 +func NewClient(apiKey *APIKey, opts ...Option) *Client { + c := &Client{ + httpClient: &http.Client{Timeout: 30 * time.Second}, + apiKey: apiKey, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// doRequest 发送请求并处理响应 +func (c *Client) doRequest(ctx context.Context, path string, reqBody interface{}) (*CommonResponse, error) { + // 序列化业务数据 + plaintext, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("marshal request body: %w", err) + } + + // SM4加密 + encryptedData, err := SM4Encrypt(c.apiKey.SM4Key, plaintext) + if err != nil { + return nil, fmt.Errorf("encrypt data: %w", err) + } + + // 生成时间戳 + timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + + // 生成签名 + sign := SM3Sign(c.apiKey.SM3Salt, timestamp+encryptedData) + + // 构建请求体 + commonReq := CommonRequestBody{EncryptedData: encryptedData} + reqBytes, err := json.Marshal(commonReq) + if err != nil { + return nil, fmt.Errorf("marshal common request: %w", err) + } + + // 创建HTTP请求 + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(reqBytes)) + 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("do request: %w", err) + } + defer resp.Body.Close() + + // 读取响应 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response body: %w", err) + } + + // 解析公共响应 + var commonResp CommonResponse + if err := json.Unmarshal(respBody, &commonResp); err != nil { + return nil, fmt.Errorf("unmarshal common response: %w", err) + } + + return &commonResp, nil +} + +// decryptData 解密响应中的data字段 +func (c *Client) decryptData(encryptedData string, target interface{}) error { + plaintext, err := SM4Decrypt(c.apiKey.SM4Key, encryptedData) + if err != nil { + return fmt.Errorf("decrypt data: %w", err) + } + if err := json.Unmarshal(plaintext, target); err != nil { + return fmt.Errorf("unmarshal decrypted data: %w", err) + } + return nil +} + +// CreateOrder 卡券/直充权益下单 +func (c *Client) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error) { + commonResp, err := c.doRequest(ctx, "/order/create", req) + if err != nil { + return nil, err + } + + if commonResp.Code != 0 { + return nil, NewSDKError(ErrorCode(commonResp.Code), commonResp.Msg, nil) + } + + if commonResp.Data == nil { + return nil, NewSDKError(ErrCodeFailure, "response data is nil", nil) + } + + var resp CreateOrderResponse + if err := c.decryptData(*commonResp.Data, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// QueryOrder 卡券/直充/微信立减金订单查询 +func (c *Client) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*QueryOrderResponse, error) { + commonResp, err := c.doRequest(ctx, "/order/query", req) + if err != nil { + return nil, err + } + + if commonResp.Code != 0 { + return nil, NewSDKError(ErrorCode(commonResp.Code), commonResp.Msg, nil) + } + + if commonResp.Data == nil { + return nil, NewSDKError(ErrCodeFailure, "response data is nil", nil) + } + + var resp QueryOrderResponse + if err := c.decryptData(*commonResp.Data, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// RechargeOrder 微信立减金订单充值 +func (c *Client) RechargeOrder(ctx context.Context, req *RechargeOrderRequest) (*RechargeOrderResponse, error) { + commonResp, err := c.doRequest(ctx, "/order/recharge", req) + if err != nil { + return nil, err + } + + if commonResp.Code != 0 { + return nil, NewSDKError(ErrorCode(commonResp.Code), commonResp.Msg, nil) + } + + if commonResp.Data == nil { + return nil, NewSDKError(ErrCodeFailure, "response data is nil", nil) + } + + var resp RechargeOrderResponse + if err := c.decryptData(*commonResp.Data, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ParseCallbackRequest 解析回调请求,返回业务数据 +func (c *Client) ParseCallbackRequest(r *http.Request) (*CallbackRequest, error) { + // 读取请求体 + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, fmt.Errorf("read request body: %w", err) + } + defer r.Body.Close() + + // 解析通用请求体 + var commonReq CommonRequestBody + if err := json.Unmarshal(body, &commonReq); err != nil { + return nil, fmt.Errorf("unmarshal common request: %w", err) + } + + // 验证签名(可选) + timestamp := r.Header.Get("timestamp") + sign := r.Header.Get("sign") + expectedSign := SM3Sign(c.apiKey.SM3Salt, timestamp+commonReq.EncryptedData) + if sign != expectedSign { + return nil, NewSDKError(ErrCodeFailure, "sign verification failed", nil) + } + + // 解密业务数据 + plaintext, err := SM4Decrypt(c.apiKey.SM4Key, commonReq.EncryptedData) + if err != nil { + return nil, fmt.Errorf("decrypt callback data: %w", err) + } + + var callbackReq CallbackRequest + if err := json.Unmarshal(plaintext, &callbackReq); err != nil { + return nil, fmt.Errorf("unmarshal callback request: %w", err) + } + + return &callbackReq, nil +} + +// CallbackHandler 返回一个http.Handler,用于处理回调通知 +func (c *Client) CallbackHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callbackReq, err := c.ParseCallbackRequest(r) + if err != nil { + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + + // 用户需要实现自己的处理逻辑,这里仅返回成功 + // 实际使用时,用户应通过闭包或回调函数注入业务处理 + _ = callbackReq + + // 返回成功 + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) +} +``` + +// File: xy-shanghai/example_test.go +```go +package xy-shanghai + +import ( + "context" + "encoding/hex" + "testing" + "time" +) + +// 测试用的密钥和盐值(实际使用时需替换为双方约定的值) +var ( + testSM4Key, _ = hex.DecodeString("0123456789abcdef0123456789abcdef") // 16字节 + testSM3Salt = []byte("test-salt") +) + +func TestCreateOrder(t *testing.T) { + apiKey := NewAPIKey(testSM4Key, testSM3Salt) + client := NewClient(apiKey, WithBaseURL("https://api.example.com"), WithTimeout(10*time.Second)) + + req := &CreateOrderRequest{ + ActCode: "ACT001", + GoodsCode: "123456", + ActOrderNum: "00001", + Account: "19912345678", + CallbackURL: "https://xxx/notice", + } + + resp, err := client.CreateOrder(context.Background(), req) + if err != nil { + t.Logf("CreateOrder error: %v", err) + // 实际测试中可能因为网络原因失败,这里仅验证逻辑 + return + } + t.Logf("CreateOrder response: %+v", resp) +} + +func TestQueryOrder(t *testing.T) { + apiKey := NewAPIKey(testSM4Key, testSM3Salt) + client := NewClient(apiKey, WithBaseURL("https://api.example.com")) + + req := &QueryOrderRequest{ + ActCode: "FBG6vdYqGE4mGX7EH/woEg==", + OrderNo: "HM1757046717684000102707339840b23222", + } + + resp, err := client.QueryOrder(context.Background(), req) + if err != nil { + t.Logf("QueryOrder error: %v", err) + return + } + t.Logf("QueryOrder response: %+v", resp) +} + +func TestRechargeOrder(t *testing.T) { + apiKey := NewAPIKey(testSM4Key, testSM3Salt) + client := NewClient(apiKey, WithBaseURL("https://api.example.com")) + + req := &RechargeOrderRequest{ + ActCode: "FBG6vdYqGE4mGX7EH/woEg==", + GoodsCode: "0001", + ActOrderNum: "0001", + OpenID: "0001", + AppID: "00001", + } + + resp, err := client.RechargeOrder(context.Background(), req) + if err != nil { + t.Logf("RechargeOrder error: %v", err) + return + } + t.Logf("RechargeOrder response: %+v", resp) +} + +func TestParseCallbackRequest(t *testing.T) { + // 模拟回调请求的构造和解析 + apiKey := NewAPIKey(testSM4Key, testSM3Salt) + client := NewClient(apiKey) + + // 构造回调业务数据 + callbackReq := &CallbackRequest{ + OrderNo: "HM202509291010001", + ActOrderNum: "XY2025092910100001", + Status: 3, + Account: "19912345678", + } + + // 加密 + plaintext, _ := json.Marshal(callbackReq) + encryptedData, _ := SM4Encrypt(apiKey.SM4Key, plaintext) + timestamp := "1700000000000" + sign := SM3Sign(apiKey.SM3Salt, timestamp+encryptedData) + + // 构造HTTP请求(简化,实际测试需使用httptest) + // 这里仅验证加密解密和签名逻辑 + t.Logf("Encrypted data: %s", encryptedData) + t.Logf("Sign: %s", sign) + + // 验证解密 + decrypted, err := SM4Decrypt(apiKey.SM4Key, encryptedData) + if err != nil { + t.Fatalf("decrypt error: %v", err) + } + t.Logf("Decrypted: %s", string(decrypted)) +} +``` \ No newline at end of file