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

16 KiB
Raw Permalink Blame History

// File: go.mod

module xy-shanghai

go 1.21

require github.com/tjfoc/gmsm v1.4.1

// File: errors.go

package xyshanghai

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: types.go

package xyshanghai

// 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: crypto.go

package xyshanghai

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: api_key.go

package xyshanghai

// 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: client.go

package xyshanghai

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: example_test.go

package xyshanghai

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"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))
}