diff --git a/xy_sh/valid.md b/xy_sh/valid.md new file mode 100644 index 0000000..1319171 --- /dev/null +++ b/xy_sh/valid.md @@ -0,0 +1,1082 @@ +// File: xy_sh/go.mod +```go +module xy_sh + +go 1.26 + +require ( + github.com/gofiber/fiber/v2 v2.52.0 + github.com/tjfoc/gmsm v1.4.1 +) +``` + +// File: xy_sh/cmd/server/main.go +```go +package main + +import ( + "log" + + "xy_sh/internal/config" + "xy_sh/internal/router" + "xy_sh/pkg/crypto" + + "github.com/gofiber/fiber/v2" +) + +func main() { + cfg := config.LoadConfig() + + crypto.InitCrypto(cfg.SM3Salt, cfg.SM4Key) + + app := fiber.New(fiber.Config{ + AppName: "XY SH API Server", + ReadTimeout: cfg.ReadTimeout, + WriteTimeout: cfg.WriteTimeout, + }) + + router.SetupRoutes(app) + + log.Printf("服务启动,监听端口: %s", cfg.ServerPort) + if err := app.Listen(":" + cfg.ServerPort); err != nil { + log.Fatalf("服务启动失败: %v", err) + } +} +``` + +// File: xy_sh/internal/config/config.go +```go +package config + +import ( + "os" + "time" +) + +type Config struct { + ServerPort string + SM3Salt string + SM4Key string + ReadTimeout time.Duration + WriteTimeout time.Duration +} + +func LoadConfig() *Config { + port := getEnv("SERVER_PORT", "8080") + sm3Salt := getEnv("SM3_SALT", "default_salt_change_me") + sm4Key := getEnv("SM4_KEY", "default_key_16by") + + return &Config{ + ServerPort: port, + SM3Salt: sm3Salt, + SM4Key: sm4Key, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} +``` + +// File: xy_sh/pkg/crypto/crypto.go +```go +package crypto + +import ( + "bytes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "io" + + "github.com/tjfoc/gmsm/sm3" + "github.com/tjfoc/gmsm/sm4" +) + +var ( + sm3Salt []byte + sm4Key []byte +) + +func InitCrypto(salt, key string) { + sm3Salt = []byte(salt) + keyBytes := []byte(key) + if len(keyBytes) < 16 { + padded := make([]byte, 16) + copy(padded, keyBytes) + sm4Key = padded + } else if len(keyBytes) > 16 { + sm4Key = keyBytes[:16] + } else { + sm4Key = keyBytes + } +} + +func SM3Hash(data []byte) string { + h := sm3.New() + h.Write(data) + hashBytes := h.Sum(nil) + return hex.EncodeToString(hashBytes) +} + +func SM3HashWithSalt(data string) string { + h := hmac.New(sm3.New, sm3Salt) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func GenerateSign(timestamp, encryptedData string) string { + signStr := timestamp + encryptedData + return SM3HashWithSalt(signStr) +} + +func VerifySign(timestamp, encryptedData, sign string) bool { + expectedSign := GenerateSign(timestamp, encryptedData) + return hmac.Equal([]byte(expectedSign), []byte(sign)) +} + +func SM4CBCEncrypt(plaintext []byte) (string, error) { + if len(sm4Key) != 16 { + return "", fmt.Errorf("SM4密钥长度必须为16字节") + } + + block, err := sm4.NewCipher(sm4Key) + if err != nil { + return "", fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + padded := pkcs7Padding(plaintext, block.BlockSize()) + + iv := make([]byte, block.BlockSize()) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", fmt.Errorf("生成IV失败: %v", err) + } + + mode := cipher.NewCBCEncrypter(block, iv) + ciphertext := make([]byte, len(padded)) + mode.CryptBlocks(ciphertext, padded) + + result := append(iv, ciphertext...) + return base64.StdEncoding.EncodeToString(result), nil +} + +func SM4CBCDecrypt(encryptedData string) ([]byte, error) { + if len(sm4Key) != 16 { + return nil, fmt.Errorf("SM4密钥长度必须为16字节") + } + + data, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("base64解码失败: %v", err) + } + + block, err := sm4.NewCipher(sm4Key) + if err != nil { + return nil, fmt.Errorf("创建SM4 cipher失败: %v", err) + } + + blockSize := block.BlockSize() + if len(data) < blockSize { + return nil, errors.New("数据长度不足") + } + iv := data[:blockSize] + ciphertext := data[blockSize:] + + if len(ciphertext)%blockSize != 0 { + return nil, errors.New("密文长度不是块大小的整数倍") + } + + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + plaintext, err = pkcs7UnPadding(plaintext) + if err != nil { + return nil, fmt.Errorf("去除填充失败: %v", err) + } + + return plaintext, nil +} + +func pkcs7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) +} + +func pkcs7UnPadding(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, errors.New("数据为空") + } + padding := int(data[length-1]) + if padding > length || padding == 0 { + return nil, errors.New("无效的填充") + } + for i := length - padding; i < length; i++ { + if data[i] != byte(padding) { + return nil, errors.New("无效的填充") + } + } + return data[:length-padding], nil +} +``` + +// File: xy_sh/internal/entities/request.go +```go +package entities + +type EncryptedRequest struct { + EncryptedData string `json:"encryptedData"` +} + +type OrderRequest struct { + ActCode string `json:"actCode"` + GoodsCode string `json:"goodsCode"` + ActOrderNum string `json:"actOrderNum"` + Account string `json:"account,omitempty"` + CallbackUrl string `json:"callbackUrl,omitempty"` +} + +type QueryOrderRequest struct { + ActCode string `json:"actCode"` + OrderNo string `json:"orderNo"` +} + +type WxRechargeRequest 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"` +} + +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"` +} + +type CardInfo struct { + CouponNo string `json:"couponNo"` + CouponCode string `json:"couponCode"` + ExpireTime string `json:"expireTime"` +} +``` + +// File: xy_sh/internal/entities/response.go +```go +package entities + +type CommonResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +type OrderResponseData struct { + OrderNo string `json:"orderNo"` + CouponNo string `json:"couponNo,omitempty"` + CouponCode string `json:"couponCode,omitempty"` + Status int `json:"status"` + ExpireTime string `json:"expireTime,omitempty"` +} + +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"` +} + +type WxRechargeResponseData struct { + OrderNo string `json:"orderNo"` + Status int `json:"status"` + CouponId string `json:"couponId,omitempty"` +} + +const ( + StatusSending = -1 + StatusSuccess = 0 + StatusFailed = 1 + StatusRedeemed = 2 + StatusExpired = 3 +) + +const ( + CodeSuccess = 0 + CodeFailed = -1 +) +``` + +// File: xy_sh/internal/middleware/middleware.go +```go +package middleware + +import ( + "encoding/json" + + "xy_sh/internal/entities" + "xy_sh/pkg/crypto" + + "github.com/gofiber/fiber/v2" +) + +func SignVerifyMiddleware() fiber.Handler { + return func(c *fiber.Ctx) error { + timestamp := c.Get("timestamp") + sign := c.Get("sign") + + if timestamp == "" || sign == "" { + return c.Status(fiber.StatusUnauthorized).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "缺少timestamp或sign请求头", + Data: nil, + }) + } + + body := c.Body() + + var req entities.EncryptedRequest + if err := json.Unmarshal(body, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "请求体格式错误", + Data: nil, + }) + } + + if req.EncryptedData == "" { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "缺少encryptedData参数", + Data: nil, + }) + } + + if !crypto.VerifySign(timestamp, req.EncryptedData, sign) { + return c.Status(fiber.StatusUnauthorized).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "签名验证失败", + Data: nil, + }) + } + + decryptedData, err := crypto.SM4CBCDecrypt(req.EncryptedData) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "数据解密失败: " + err.Error(), + Data: nil, + }) + } + + c.Locals("decryptedData", decryptedData) + c.Locals("timestamp", timestamp) + + return c.Next() + } +} + +func EncryptResponseMiddleware() fiber.Handler { + return func(c *fiber.Ctx) error { + err := c.Next() + if err != nil { + return err + } + + body := c.Response().Body() + if len(body) == 0 { + return nil + } + + var resp entities.CommonResponse + if err := json.Unmarshal(body, &resp); err != nil { + return nil + } + + if resp.Data != nil { + dataBytes, err := json.Marshal(resp.Data) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "响应数据序列化失败", + Data: nil, + }) + } + + encryptedData, err := crypto.SM4CBCEncrypt(dataBytes) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "响应数据加密失败", + Data: nil, + }) + } + + resp.Data = encryptedData + + newBody, err := json.Marshal(resp) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "响应序列化失败", + Data: nil, + }) + } + + c.Response().SetBody(newBody) + } + + return nil + } +} +``` + +// File: xy_sh/internal/biz/biz.go +```go +package biz + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "io" + "strconv" + "sync" + "time" + + "xy_sh/internal/entities" +) + +type OrderStore struct { + mu sync.RWMutex + orders map[string]*OrderInfo +} + +type OrderInfo struct { + OrderNo string + ActOrderNum string + GoodsCode string + ActCode string + Account string + Status int + CouponNo string + CouponCode string + ExpireTime string + CouponId string + AppId string + OpenId string + CreateTime time.Time + UpdateTime time.Time +} + +var store *OrderStore + +func init() { + store = &OrderStore{ + orders: make(map[string]*OrderInfo), + } +} + +func CreateOrder(req *entities.OrderRequest) (*entities.OrderResponseData, error) { + store.mu.Lock() + defer store.mu.Unlock() + + orderNo := generateOrderNo() + + orderInfo := &OrderInfo{ + OrderNo: orderNo, + ActOrderNum: req.ActOrderNum, + GoodsCode: req.GoodsCode, + ActCode: req.ActCode, + Account: req.Account, + Status: entities.StatusSuccess, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + + orderInfo.CouponNo = generateCouponNo() + orderInfo.CouponCode = orderInfo.CouponNo + orderInfo.ExpireTime = time.Now().AddDate(1, 0, 0).Format("2006-01-02 15:04:05") + + store.orders[orderNo] = orderInfo + + return &entities.OrderResponseData{ + OrderNo: orderNo, + CouponNo: orderInfo.CouponNo, + CouponCode: orderInfo.CouponCode, + Status: orderInfo.Status, + ExpireTime: orderInfo.ExpireTime, + }, nil +} + +func QueryOrder(req *entities.QueryOrderRequest) (*entities.QueryOrderResponseData, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + orderInfo, exists := store.orders[req.OrderNo] + if !exists { + return nil, errors.New("订单不存在") + } + + resp := &entities.QueryOrderResponseData{ + OrderNo: orderInfo.OrderNo, + Status: orderInfo.Status, + Account: orderInfo.Account, + CouponId: orderInfo.CouponId, + } + + if orderInfo.CouponNo != "" { + resp.CardInfo = &entities.CardInfo{ + CouponNo: orderInfo.CouponNo, + CouponCode: orderInfo.CouponCode, + ExpireTime: orderInfo.ExpireTime, + } + } + + return resp, nil +} + +func WxRecharge(req *entities.WxRechargeRequest) (*entities.WxRechargeResponseData, error) { + store.mu.Lock() + defer store.mu.Unlock() + + if orderInfo, exists := store.orders[req.OrderNo]; exists { + return &entities.WxRechargeResponseData{ + OrderNo: orderInfo.OrderNo, + Status: orderInfo.Status, + CouponId: orderInfo.CouponId, + }, nil + } + + orderNo := req.OrderNo + + orderInfo := &OrderInfo{ + OrderNo: orderNo, + ActOrderNum: req.ActOrderNum, + GoodsCode: req.GoodsCode, + ActCode: req.ActCode, + AppId: req.AppId, + OpenId: req.OpenId, + Status: entities.StatusSuccess, + CouponId: generateCouponId(), + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + + store.orders[orderNo] = orderInfo + + return &entities.WxRechargeResponseData{ + OrderNo: orderNo, + Status: orderInfo.Status, + CouponId: orderInfo.CouponId, + }, nil +} + +func HandleCallback(req *entities.CallbackRequest) error { + store.mu.Lock() + defer store.mu.Unlock() + + orderInfo, exists := store.orders[req.OrderNo] + if !exists { + orderInfo = &OrderInfo{ + OrderNo: req.OrderNo, + ActOrderNum: req.ActOrderNum, + CreateTime: time.Now(), + } + store.orders[req.OrderNo] = orderInfo + } + + orderInfo.Status = req.Status + orderInfo.Account = req.Account + orderInfo.CouponId = req.CouponId + orderInfo.UpdateTime = time.Now() + + if req.CardInfo != nil { + orderInfo.CouponNo = req.CardInfo.CouponNo + orderInfo.CouponCode = req.CardInfo.CouponCode + orderInfo.ExpireTime = req.CardInfo.ExpireTime + } + + return nil +} + +func generateOrderNo() string { + return "HM" + strconv.FormatInt(time.Now().UnixMilli(), 10) + randomString(16) +} + +func generateCouponNo() string { + return randomString(16) +} + +func generateCouponId() string { + return randomString(8) +} + +func randomString(n int) string { + b := make([]byte, (n+1)/2) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + panic(err) + } + return hex.EncodeToString(b)[:n] +} +``` + +// File: xy_sh/internal/service/service.go +```go +package service + +import ( + "encoding/json" + + "xy_sh/internal/biz" + "xy_sh/internal/entities" + + "github.com/gofiber/fiber/v2" +) + +func CreateOrderHandler(c *fiber.Ctx) error { + decryptedData := c.Locals("decryptedData").([]byte) + + var req entities.OrderRequest + if err := json.Unmarshal(decryptedData, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "业务参数解析失败: " + err.Error(), + Data: nil, + }) + } + + if req.ActCode == "" || req.GoodsCode == "" || req.ActOrderNum == "" { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "缺少必填参数: actCode, goodsCode, actOrderNum", + Data: nil, + }) + } + + result, err := biz.CreateOrder(&req) + if err != nil { + return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: err.Error(), + Data: nil, + }) + } + + return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{ + Code: entities.CodeSuccess, + Msg: "请求成功", + Data: result, + }) +} + +func QueryOrderHandler(c *fiber.Ctx) error { + decryptedData := c.Locals("decryptedData").([]byte) + + var req entities.QueryOrderRequest + if err := json.Unmarshal(decryptedData, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "业务参数解析失败: " + err.Error(), + Data: nil, + }) + } + + if req.ActCode == "" || req.OrderNo == "" { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "缺少必填参数: actCode, orderNo", + Data: nil, + }) + } + + result, err := biz.QueryOrder(&req) + if err != nil { + return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: err.Error(), + Data: nil, + }) + } + + return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{ + Code: entities.CodeSuccess, + Msg: "请求成功", + Data: result, + }) +} + +func WxRechargeHandler(c *fiber.Ctx) error { + decryptedData := c.Locals("decryptedData").([]byte) + + var req entities.WxRechargeRequest + if err := json.Unmarshal(decryptedData, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "业务参数解析失败: " + err.Error(), + Data: nil, + }) + } + + if req.ActCode == "" || req.OrderNo == "" || req.GoodsCode == "" || req.ActOrderNum == "" || + req.AppId == "" || req.OpenId == "" { + return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: "缺少必填参数: actCode, orderNo, goodsCode, actOrderNum, appId, openId", + Data: nil, + }) + } + + result, err := biz.WxRecharge(&req) + if err != nil { + return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{ + Code: entities.CodeFailed, + Msg: err.Error(), + Data: nil, + }) + } + + return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{ + Code: entities.CodeSuccess, + Msg: "请求成功", + Data: result, + }) +} + +func CallbackHandler(c *fiber.Ctx) error { + decryptedData := c.Locals("decryptedData").([]byte) + + var req entities.CallbackRequest + if err := json.Unmarshal(decryptedData, &req); err != nil { + return c.Status(fiber.StatusBadRequest).SendString("参数解析失败") + } + + if req.OrderNo == "" || req.ActOrderNum == "" { + return c.Status(fiber.StatusBadRequest).SendString("缺少必填参数") + } + + if err := biz.HandleCallback(&req); err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("处理失败") + } + + return c.Status(fiber.StatusOK).SendString("ok") +} + +func HealthHandler(c *fiber.Ctx) error { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "status": "ok", + "message": "服务运行正常", + }) +} +``` + +// File: xy_sh/internal/router/router.go +```go +package router + +import ( + "xy_sh/internal/middleware" + "xy_sh/internal/service" + + "github.com/gofiber/fiber/v2" +) + +func SetupRoutes(app *fiber.App) { + app.Get("/health", service.HealthHandler) + app.Get("/", service.HealthHandler) + + v1 := app.Group("/api/v1") + + v1.Post("/order/create", + middleware.SignVerifyMiddleware(), + middleware.EncryptResponseMiddleware(), + service.CreateOrderHandler, + ) + + v1.Post("/order/query", + middleware.SignVerifyMiddleware(), + middleware.EncryptResponseMiddleware(), + service.QueryOrderHandler, + ) + + v1.Post("/wx/recharge", + middleware.SignVerifyMiddleware(), + middleware.EncryptResponseMiddleware(), + service.WxRechargeHandler, + ) + + v1.Post("/callback/notify", + middleware.SignVerifyMiddleware(), + service.CallbackHandler, + ) +} +``` + +// File: xy_sh/internal/test/example_test.go +```go +package test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "xy_sh/internal/entities" + "xy_sh/internal/router" + "xy_sh/pkg/crypto" + + "github.com/gofiber/fiber/v2" +) + +func init() { + crypto.InitCrypto("test_salt_12345", "test_key_16bytes") +} + +func setupTestApp() *fiber.App { + app := fiber.New() + router.SetupRoutes(app) + return app +} + +func makeEncryptedRequest(t *testing.T, app *fiber.App, path string, bizData interface{}) (*http.Response, []byte) { + t.Helper() + + bizJSON, err := json.Marshal(bizData) + if err != nil { + t.Fatalf("业务数据序列化失败: %v", err) + } + + encryptedData, err := crypto.SM4CBCEncrypt(bizJSON) + if err != nil { + t.Fatalf("加密失败: %v", err) + } + + timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + + sign := crypto.GenerateSign(timestamp, encryptedData) + + reqBody := entities.EncryptedRequest{ + EncryptedData: encryptedData, + } + reqJSON, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(reqJSON)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("timestamp", timestamp) + req.Header.Set("sign", sign) + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + return resp, body +} + +func TestCreateOrder(t *testing.T) { + app := setupTestApp() + + bizData := entities.OrderRequest{ + ActCode: "ACT001", + GoodsCode: "123456", + ActOrderNum: "TEST_ORDER_001", + Account: "19912345678", + CallbackUrl: "https://example.com/callback", + } + + resp, body := makeEncryptedRequest(t, app, "/api/v1/order/create", bizData) + + if resp.StatusCode != http.StatusOK { + t.Errorf("期望状态码200,实际: %d, 响应: %s", resp.StatusCode, string(body)) + } + + var commonResp entities.CommonResponse + if err := json.Unmarshal(body, &commonResp); err != nil { + t.Fatalf("响应解析失败: %v, 响应: %s", err, string(body)) + } + + if commonResp.Code != entities.CodeSuccess { + t.Errorf("期望code=0,实际: %d, msg: %s", commonResp.Code, commonResp.Msg) + } + + t.Logf("下单接口测试成功,响应: %s", string(body)) +} + +func TestQueryOrder(t *testing.T) { + app := setupTestApp() + + createBizData := entities.OrderRequest{ + ActCode: "ACT001", + GoodsCode: "123456", + ActOrderNum: "TEST_ORDER_002", + Account: "19912345678", + } + + _, createBody := makeEncryptedRequest(t, app, "/api/v1/order/create", createBizData) + + var createResp entities.CommonResponse + if err := json.Unmarshal(createBody, &createResp); err != nil { + t.Fatalf("创建订单响应解析失败: %v", err) + } + + if createResp.Code != entities.CodeSuccess { + t.Fatalf("创建订单失败: %s", createResp.Msg) + } + + encryptedData := createResp.Data.(string) + decryptedData, err := crypto.SM4CBCDecrypt(encryptedData) + if err != nil { + t.Fatalf("解密创建订单响应失败: %v", err) + } + + var orderResp entities.OrderResponseData + if err := json.Unmarshal(decryptedData, &orderResp); err != nil { + t.Fatalf("解析创建订单业务数据失败: %v", err) + } + + queryBizData := entities.QueryOrderRequest{ + ActCode: "ACT001", + OrderNo: orderResp.OrderNo, + } + + resp, body := makeEncryptedRequest(t, app, "/api/v1/order/query", queryBizData) + + if resp.StatusCode != http.StatusOK { + t.Errorf("期望状态码200,实际: %d, 响应: %s", resp.StatusCode, string(body)) + } + + var queryResp entities.CommonResponse + if err := json.Unmarshal(body, &queryResp); err != nil { + t.Fatalf("查询响应解析失败: %v", err) + } + + if queryResp.Code != entities.CodeSuccess { + t.Errorf("查询失败,code: %d, msg: %s", queryResp.Code, queryResp.Msg) + } + + t.Logf("查询接口测试成功,响应: %s", string(body)) +} + +func TestWxRecharge(t *testing.T) { + app := setupTestApp() + + bizData := entities.WxRechargeRequest{ + ActCode: "FBG6vdYqGE4mGX7EH/woEg==", + OrderNo: "WX_TEST_ORDER_001", + GoodsCode: "0001", + ActOrderNum: "WX_ORDER_001", + AppId: "wx1234567890", + OpenId: "oABC1234567890", + } + + resp, body := makeEncryptedRequest(t, app, "/api/v1/wx/recharge", bizData) + + if resp.StatusCode != http.StatusOK { + t.Errorf("期望状态码200,实际: %d, 响应: %s", resp.StatusCode, string(body)) + } + + var commonResp entities.CommonResponse + if err := json.Unmarshal(body, &commonResp); err != nil { + t.Fatalf("响应解析失败: %v", err) + } + + if commonResp.Code != entities.CodeSuccess { + t.Errorf("期望code=0,实际: %d, msg: %s", commonResp.Code, commonResp.Msg) + } + + t.Logf("微信立减金充值接口测试成功,响应: %s", string(body)) +} + +func TestCallback(t *testing.T) { + app := setupTestApp() + + bizData := entities.CallbackRequest{ + OrderNo: "HM202509291010001", + ActOrderNum: "XY2025092910100001", + Status: entities.StatusExpired, + Account: "19912345678", + } + + resp, body := makeEncryptedRequest(t, app, "/api/v1/callback/notify", bizData) + + if resp.StatusCode != http.StatusOK { + t.Errorf("期望状态码200,实际: %d, 响应: %s", resp.StatusCode, string(body)) + } + + if string(body) != "ok" { + t.Errorf("期望响应'ok',实际: %s", string(body)) + } + + t.Logf("回调通知接口测试成功") +} + +func TestInvalidSign(t *testing.T) { + app := setupTestApp() + + bizData := entities.OrderRequest{ + ActCode: "ACT001", + GoodsCode: "123456", + ActOrderNum: "TEST_ORDER_003", + } + + bizJSON, _ := json.Marshal(bizData) + encryptedData, _ := crypto.SM4CBCEncrypt(bizJSON) + timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + + wrongSign := "wrong_signature_12345" + + reqBody := entities.EncryptedRequest{EncryptedData: encryptedData} + reqJSON, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/order/create", bytes.NewReader(reqJSON)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("timestamp", timestamp) + req.Header.Set("sign", wrongSign) + + resp, _ := app.Test(req) + + if resp.StatusCode != http.StatusUnauthorized { + t.Errorf("期望状态码401(签名验证失败),实际: %d", resp.StatusCode) + } + + t.Logf("签名验证失败测试成功") +} + +func TestHealthCheck(t *testing.T) { + app := setupTestApp() + + req := httptest.NewRequest(http.MethodGet, "/health", nil) + resp, body := app.Test(req) + + if resp.StatusCode != http.StatusOK { + t.Errorf("期望状态码200,实际: %d", resp.StatusCode) + } + + t.Logf("健康检查接口测试成功,响应: %s", string(body)) +} +``` \ No newline at end of file