xy-shanghai-20260721-181118/xy-shanghai/generate.md

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