package routes import ( "context" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "sync/atomic" "testing" "time" "github.com/bytedance/sonic" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/bootstrap" "github.com/break/junhong_cmp_fiber/internal/handler/admin" internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware" "github.com/break/junhong_cmp_fiber/internal/model" shopService "github.com/break/junhong_cmp_fiber/internal/service/shop" shopCommissionService "github.com/break/junhong_cmp_fiber/internal/service/shop_commission" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/auth" "github.com/break/junhong_cmp_fiber/pkg/config" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/database" "github.com/break/junhong_cmp_fiber/pkg/errors" pkgMiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware" ) type shopTestResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data shopTestPageData `json:"data"` } type shopTestPageData struct { Items []map[string]any `json:"items"` Total int64 `json:"total"` Page int `json:"page"` Size int `json:"size"` } type shopTestEnv struct { app *fiber.App db *gorm.DB redis *redis.Client tokenManager *auth.TokenManager } // TestShopListValidatesAllQueryParameters 验证店铺列表会完整校验所有查询参数。 func TestShopListValidatesAllQueryParameters(t *testing.T) { env := newShopTestEnv(t) token := env.newToken(t, auth.TokenInfo{ UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin", }) testCases := []struct { name string query string }{ {name: "页码小于一", query: "page=0"}, {name: "每页数量小于一", query: "page_size=0"}, {name: "每页数量超过上限", query: "page_size=101"}, {name: "店铺层级非法", query: "level=8"}, {name: "店铺状态非法", query: "status=2"}, {name: "店铺名称过长", query: "shop_name=" + strings.Repeat("店", 101)}, {name: "店铺编号过长", query: "shop_code=" + strings.Repeat("A", 51)}, {name: "参数解析失败", query: "parent_id=invalid"}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { status, body := env.request(t, http.MethodGet, "/api/admin/shops?"+testCase.query, token) if status != http.StatusBadRequest { t.Fatalf("期望 HTTP 400,实际为 %d,响应:%s", status, body) } var response struct { Code int `json:"code"` Msg string `json:"msg"` Data any `json:"data"` } if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析响应失败:%v", err) } if response.Code != errors.CodeInvalidParam || response.Msg != "参数验证失败" || response.Data != nil { t.Fatalf("参数错误响应不符合契约:%s", body) } if !strings.Contains(string(body), "timestamp") { t.Fatalf("参数错误响应缺少时间戳:%s", body) } if strings.Contains(string(body), "validation") || strings.Contains(string(body), "strconv") { t.Fatalf("参数错误响应泄露底层细节:%s", body) } }) } } // TestShopListUsesNormalizedAndExplicitPagination 验证默认分页归一化且显式分页保持不变。 func TestShopListUsesNormalizedAndExplicitPagination(t *testing.T) { env := newShopTestEnv(t) token := env.newToken(t, auth.TokenInfo{ UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin", }) testCases := []struct { name string query string expectedPage int expectedSize int }{ {name: "默认分页", expectedPage: constants.DefaultPage, expectedSize: constants.DefaultPageSize}, {name: "显式分页", query: "?page=2&page_size=7&shop_name=ur60-no-match", expectedPage: 2, expectedSize: 7}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { status, body := env.request(t, http.MethodGet, "/api/admin/shops"+testCase.query, token) if status != http.StatusOK { t.Fatalf("期望 HTTP 200,实际为 %d,响应:%s", status, body) } var response shopTestResponse if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析响应失败:%v", err) } if response.Code != errors.CodeSuccess || response.Data.Page != testCase.expectedPage || response.Data.Size != testCase.expectedSize { t.Fatalf("分页响应不符合契约:%s", body) } }) } } // TestShopListKeepsAuthenticationContract 验证店铺列表保持既有认证错误契约。 func TestShopListKeepsAuthenticationContract(t *testing.T) { env := newShopTestEnv(t) testCases := []struct { name string token string }{ {name: "未认证"}, {name: "无效令牌", token: "ur60-invalid-token"}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { status, body := env.request(t, http.MethodGet, "/api/admin/shops", testCase.token) if status != http.StatusUnauthorized { t.Fatalf("期望 HTTP 401,实际为 %d,响应:%s", status, body) } if !strings.Contains(string(body), "timestamp") { t.Fatalf("认证错误响应缺少时间戳:%s", body) } }) } } // TestEnterpriseCannotAccessCoreShopManagementRoutes 验证企业账号无法访问五个核心店铺管理入口。 func TestEnterpriseCannotAccessCoreShopManagementRoutes(t *testing.T) { env := newShopTestEnv(t) token := env.newToken(t, auth.TokenInfo{ UserID: uniqueTestID(), UserType: constants.UserTypeEnterprise, EnterpriseID: uniqueTestID(), Username: "ur60-enterprise", }) testCases := []struct { name string method string path string }{ {name: "店铺列表", method: http.MethodGet, path: "/api/admin/shops"}, {name: "创建店铺", method: http.MethodPost, path: "/api/admin/shops"}, {name: "更新店铺", method: http.MethodPut, path: "/api/admin/shops/1"}, {name: "删除店铺", method: http.MethodDelete, path: "/api/admin/shops/1"}, {name: "联级查询", method: http.MethodGet, path: "/api/admin/shops/cascade"}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { status, body := env.request(t, testCase.method, testCase.path, token) if status != http.StatusForbidden { t.Fatalf("期望 HTTP 403,实际为 %d,响应:%s", status, body) } var response struct { Code int `json:"code"` Msg string `json:"msg"` Data any `json:"data"` } if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析响应失败:%v", err) } if response.Code != errors.CodeForbidden || response.Msg != constants.ShopManagementForbiddenMessage || response.Data != nil { t.Fatalf("企业账号禁止响应不符合契约:%s", body) } if !strings.Contains(string(body), "timestamp") { t.Fatalf("企业账号禁止响应缺少时间戳:%s", body) } }) } } // TestCoreShopRestrictionDoesNotAffectOtherShopRoutes 验证核心店铺权限限制不影响其他店铺前缀路由。 func TestCoreShopRestrictionDoesNotAffectOtherShopRoutes(t *testing.T) { env := newShopTestEnv(t) token := env.newToken(t, auth.TokenInfo{ UserID: uniqueTestID(), UserType: constants.UserTypeEnterprise, EnterpriseID: uniqueTestID(), Username: "ur60-enterprise", }) paths := []string{ "/api/admin/shops/1/roles", "/api/admin/shops/fund-summary", "/api/admin/shops/1/withdrawal-requests", "/api/admin/shops/1/commission-records", "/api/admin/shops/1/main-wallet/transactions", } for _, path := range paths { status, body := env.request(t, http.MethodGet, path, token) if status == http.StatusForbidden && strings.Contains(string(body), constants.ShopManagementForbiddenMessage) { t.Fatalf("核心店铺管理拦截误伤独立路由 %s:%s", path, body) } } } // TestNonEnterpriseAccountsKeepCoreShopListAccess 验证非企业账号保留核心店铺列表访问能力。 func TestNonEnterpriseAccountsKeepCoreShopListAccess(t *testing.T) { env := newShopTestEnv(t) testCases := []struct { name string userType int shopID uint }{ {name: "超级管理员", userType: constants.UserTypeSuperAdmin}, {name: "平台账号", userType: constants.UserTypePlatform}, {name: "代理账号", userType: constants.UserTypeAgent, shopID: 1}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { token := env.newToken(t, auth.TokenInfo{ UserID: uniqueTestID(), UserType: testCase.userType, ShopID: testCase.shopID, Username: "ur60-allowed-user", }) status, body := env.request(t, http.MethodGet, "/api/admin/shops?page=1&page_size=1", token) if status != http.StatusOK { t.Fatalf("期望保留列表访问能力,实际 HTTP %d,响应:%s", status, body) } }) } } // TestShopListFiltersContactPhoneExactly 验证联系电话精确匹配、AND 组合、重复号码、排序及软删除语义。 func TestShopListFiltersContactPhoneExactly(t *testing.T) { env := newShopTestEnv(t) phone := "13800000000" parent := env.createShop(t, "UR60父店", phone, nil, 1, time.Now().Add(-4*time.Minute)) newer := env.createShop(t, "UR60目标新店", phone, &parent.ID, 2, time.Now().Add(-time.Minute)) older := env.createShop(t, "UR60目标旧店", phone, &parent.ID, 2, time.Now().Add(-2*time.Minute)) env.createShop(t, "UR60相似号码", "13800000001", &parent.ID, 2, time.Now()) deleted := env.createShop(t, "UR60软删除店", phone, &parent.ID, 2, time.Now().Add(time.Minute)) if err := env.db.Delete(deleted).Error; err != nil { t.Fatalf("软删除测试店铺失败:%v", err) } token := env.newToken(t, auth.TokenInfo{ UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin", }) status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone+"&shop_name=UR60目标&parent_id="+strconv.FormatUint(uint64(parent.ID), 10)+"&level=2&status=1", token) if status != http.StatusOK { t.Fatalf("联系电话组合查询失败,HTTP %d:%s", status, body) } var response shopTestResponse if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析联系电话查询响应失败:%v", err) } if response.Data.Total != 2 || len(response.Data.Items) != 2 { t.Fatalf("联系电话组合查询应返回两条可见记录:%s", body) } if uint(response.Data.Items[0]["id"].(float64)) != newer.ID || uint(response.Data.Items[1]["id"].(float64)) != older.ID { t.Fatalf("联系电话查询未保持 created_at DESC:%s", body) } status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=13800000001&shop_code="+newer.ShopCode, token) if status != http.StatusOK { t.Fatalf("联系电话精确性查询失败,HTTP %d:%s", status, body) } if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析联系电话精确性响应失败:%v", err) } if response.Data.Total != 0 { t.Fatalf("联系电话必须与其他条件按 AND 精确匹配:%s", body) } platformToken := env.newToken(t, auth.TokenInfo{ UserID: uniqueTestID(), UserType: constants.UserTypePlatform, Username: "ur60-platform", }) status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone+"&shop_name=UR60目标", platformToken) if status != http.StatusOK { t.Fatalf("平台账号联系电话查询失败,HTTP %d:%s", status, body) } if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析平台账号联系电话响应失败:%v", err) } if response.Data.Total != 2 { t.Fatalf("平台账号联系电话查询结果不符合契约:%s", body) } } // TestShopListContactPhoneKeepsEmptyAndPaginationSemantics 验证空电话、空结果和显式分页契约。 func TestShopListContactPhoneKeepsEmptyAndPaginationSemantics(t *testing.T) { env := newShopTestEnv(t) token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"}) status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=&page=2&page_size=3", token) if status != http.StatusOK { t.Fatalf("空联系电话不应改变列表语义,HTTP %d:%s", status, body) } var response shopTestResponse if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析空联系电话响应失败:%v", err) } if response.Data.Page != 2 || response.Data.Size != 3 { t.Fatalf("联系电话筛选不应重置显式分页:%s", body) } status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=19999999999", token) if status != http.StatusOK { t.Fatalf("无匹配联系电话应返回成功空分页,HTTP %d:%s", status, body) } if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析空结果响应失败:%v", err) } if response.Data.Total != 0 || len(response.Data.Items) != 0 { t.Fatalf("无匹配联系电话响应不符合空分页契约:%s", body) } } // TestShopListRejectsInvalidContactPhoneBeforeQuery 验证非法联系电话在执行店铺查询前被拒绝。 func TestShopListRejectsInvalidContactPhoneBeforeQuery(t *testing.T) { env := newShopTestEnv(t) token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"}) var shopQueryCount atomic.Int64 callbackName := fmt.Sprintf("ur60:count_shop_queries:%d", time.Now().UnixNano()) if err := env.db.Callback().Query().Before("gorm:query").Register(callbackName, func(db *gorm.DB) { if db.Statement != nil && db.Statement.Table == (model.Shop{}).TableName() { shopQueryCount.Add(1) } }); err != nil { t.Fatalf("注册店铺查询计数回调失败:%v", err) } t.Cleanup(func() { _ = env.db.Callback().Query().Remove(callbackName) }) invalidPhones := []string{ "1380000000", "138000000000", "1380000000A", "+8613800000000", "138-0000-0000", " 13800000000", "13800000000", } for _, phone := range invalidPhones { shopQueryCount.Store(0) status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+url.QueryEscape(phone), token) if status != http.StatusBadRequest || shopQueryCount.Load() != 0 { t.Fatalf("非法联系电话应在查询前返回 400,phone=%q, queries=%d, body=%s", phone, shopQueryCount.Load(), body) } } } // TestAgentContactPhoneSearchKeepsShopScope 验证代理账号无法通过联系电话越过店铺层级范围。 func TestAgentContactPhoneSearchKeepsShopScope(t *testing.T) { env := newShopTestEnv(t) phone := "13700000000" root := env.createShop(t, "UR60代理根店", phone, nil, 1, time.Now().Add(-3*time.Minute)) child := env.createShop(t, "UR60代理下级", phone, &root.ID, 2, time.Now().Add(-2*time.Minute)) env.createShop(t, "UR60范围外店铺", phone, nil, 1, time.Now().Add(-time.Minute)) token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeAgent, ShopID: root.ID, Username: "ur60-agent"}) status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone, token) if status != http.StatusOK { t.Fatalf("代理联系电话查询失败,HTTP %d:%s", status, body) } var response shopTestResponse if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析代理联系电话响应失败:%v", err) } if response.Data.Total != 2 { t.Fatalf("代理联系电话查询应只返回自身及下级:%s", body) } returned := map[uint]bool{} for _, item := range response.Data.Items { returned[uint(item["id"].(float64))] = true } if !returned[root.ID] || !returned[child.ID] { t.Fatalf("代理联系电话查询缺少范围内店铺:%s", body) } } // TestShopListDatabaseFailureIsSanitized 验证数据库错误通过统一 500 响应脱敏。 func TestShopListDatabaseFailureIsSanitized(t *testing.T) { env := newShopTestEnv(t) token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"}) sqlDB, err := env.db.DB() if err != nil { t.Fatalf("获取测试数据库连接失败:%v", err) } if err := sqlDB.Close(); err != nil { t.Fatalf("关闭测试数据库连接失败:%v", err) } status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=13800000000", token) if status != http.StatusInternalServerError { t.Fatalf("数据库失败应返回 HTTP 500,实际 %d:%s", status, body) } var response struct { Code int `json:"code"` Msg string `json:"msg"` Data any `json:"data"` } if err := sonic.Unmarshal(body, &response); err != nil { t.Fatalf("解析数据库错误响应失败:%v", err) } if response.Code != errors.CodeInternalError || response.Msg != "内部服务器错误" || response.Data != nil { t.Fatalf("数据库错误响应不符合脱敏契约:%s", body) } for _, leaked := range []string{"sql", "postgres", "host", "driver", "database is closed"} { if strings.Contains(strings.ToLower(string(body)), leaked) { t.Fatalf("数据库错误响应泄露底层信息 %q:%s", leaked, body) } } } // TestShopContactPhoneIndexDefinition 验证联系电话索引的类型、唯一性和部分条件。 func TestShopContactPhoneIndexDefinition(t *testing.T) { env := newShopTestEnv(t) var indexCount int64 if err := env.db.Raw(` SELECT COUNT(*) FROM pg_class WHERE relname = ? `, "idx_shop_contact_phone").Scan(&indexCount).Error; err != nil { t.Fatalf("查询联系电话索引数量失败:%v", err) } if os.Getenv("UR60_EXPECT_INDEX_ABSENT") == "1" { if indexCount != 0 { t.Fatalf("回滚后联系电话索引仍然存在") } return } if indexCount != 1 { t.Fatalf("联系电话索引数量异常:%d", indexCount) } var index struct { AccessMethod string `gorm:"column:access_method"` IsUnique bool `gorm:"column:is_unique"` Definition string `gorm:"column:definition"` Predicate string `gorm:"column:predicate"` } err := env.db.Raw(` SELECT am.amname AS access_method, ix.indisunique AS is_unique, pg_get_indexdef(ix.indexrelid) AS definition, pg_get_expr(ix.indpred, ix.indrelid) AS predicate FROM pg_index ix JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_am am ON am.oid = i.relam WHERE i.relname = ? `, "idx_shop_contact_phone").Scan(&index).Error if err != nil { t.Fatalf("查询联系电话索引定义失败:%v", err) } if index.AccessMethod != "btree" || index.IsUnique || !strings.Contains(index.Definition, "contact_phone") || !strings.Contains(index.Predicate, "deleted_at IS NULL") { t.Fatalf("联系电话索引定义不符合契约:%+v", index) } } func newShopTestEnv(t *testing.T) *shopTestEnv { t.Helper() if os.Getenv("JUNHONG_DATABASE_HOST") == "" || os.Getenv("JUNHONG_REDIS_ADDRESS") == "" { t.Skip("未加载 .env.local,跳过依赖真实 PostgreSQL 和 Redis 的店铺 HTTP 集成测试") } cfg, err := config.Load() if err != nil { t.Fatalf("加载测试配置失败:%v", err) } logger := zap.NewNop() db, err := database.InitPostgreSQL(&cfg.Database, logger) if err != nil { t.Fatalf("连接 PostgreSQL 失败:%v", err) } redisClient, err := database.NewRedisClient(database.RedisConfig{ Address: cfg.Redis.Address + ":" + strconv.Itoa(cfg.Redis.Port), Password: cfg.Redis.Password, DB: cfg.Redis.DB, PoolSize: cfg.Redis.PoolSize, MinIdleConns: cfg.Redis.MinIdleConns, DialTimeout: cfg.Redis.DialTimeout, ReadTimeout: cfg.Redis.ReadTimeout, WriteTimeout: cfg.Redis.WriteTimeout, }, logger) if err != nil { t.Fatalf("连接 Redis 失败:%v", err) } t.Cleanup(func() { _ = redisClient.Close() if sqlDB, dbErr := db.DB(); dbErr == nil { _ = sqlDB.Close() } }) shopStore := postgres.NewShopStore(db, redisClient) service := shopService.New(shopStore, nil, nil, nil, nil, nil) shopCommission := shopCommissionService.New( shopStore, postgres.NewAccountStore(db, redisClient), postgres.NewAgentWalletStore(db, redisClient), postgres.NewCommissionWithdrawalRequestStore(db, redisClient), postgres.NewCommissionWithdrawalSettingStore(db, redisClient), postgres.NewCommissionRecordStore(db, redisClient), postgres.NewAgentWalletTransactionStore(db, redisClient), db, logger, ) tokenManager := auth.NewTokenManager(redisClient, cfg.JWT.AccessTokenTTL, cfg.JWT.RefreshTokenTTL) authMiddleware := pkgMiddleware.Auth(pkgMiddleware.AuthConfig{ TokenValidator: func(token string) (*pkgMiddleware.UserContextInfo, error) { info, validateErr := tokenManager.ValidateAccessToken(context.Background(), token) if validateErr != nil { return nil, errors.New(errors.CodeInvalidToken, "认证令牌无效或已过期") } return &pkgMiddleware.UserContextInfo{ UserID: info.UserID, UserType: info.UserType, Username: info.Username, ShopID: info.ShopID, EnterpriseID: info.EnterpriseID, }, nil }, ShopStore: shopStore, }) app := fiber.New(fiber.Config{ JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, ErrorHandler: internalMiddleware.ErrorHandler(logger), }) RegisterAdminRoutes(app.Group("/api/admin"), &bootstrap.Handlers{ Shop: admin.NewShopHandler(service, validator.New()), ShopRole: admin.NewShopRoleHandler(service), ShopCommission: admin.NewShopCommissionHandler(shopCommission), }, &bootstrap.Middlewares{AdminAuth: authMiddleware}, nil, "/api/admin") return &shopTestEnv{app: app, db: db, redis: redisClient, tokenManager: tokenManager} } func (e *shopTestEnv) createShop(t *testing.T, name, phone string, parentID *uint, level int, createdAt time.Time) *model.Shop { t.Helper() shop := &model.Shop{ ShopName: name, ShopCode: fmt.Sprintf("UR60-%d", time.Now().UnixNano()), ParentID: parentID, Level: level, ContactPhone: phone, Status: constants.ShopStatusEnabled, } shop.CreatedAt = createdAt shop.UpdatedAt = createdAt shop.Creator = uniqueTestID() shop.Updater = shop.Creator if err := e.db.Create(shop).Error; err != nil { t.Fatalf("创建测试店铺失败:%v", err) } t.Cleanup(func() { _ = e.db.Unscoped().Delete(shop).Error }) return shop } func (e *shopTestEnv) newToken(t *testing.T, info auth.TokenInfo) string { t.Helper() accessToken, refreshToken, err := e.tokenManager.GenerateTokenPair(context.Background(), &info) if err != nil { t.Fatalf("创建测试令牌失败:%v", err) } t.Cleanup(func() { _ = e.tokenManager.RevokeToken(context.Background(), accessToken) _ = e.tokenManager.RevokeToken(context.Background(), refreshToken) _ = e.redis.Del(context.Background(), constants.RedisUserTokensKey(info.UserID)).Err() }) return accessToken } func (e *shopTestEnv) request(t *testing.T, method, path, token string) (int, []byte) { t.Helper() request, err := http.NewRequest(method, path, nil) if err != nil { t.Fatalf("创建 HTTP 请求失败:%v", err) } if token != "" { request.Header.Set(fiber.HeaderAuthorization, "Bearer "+token) } response, err := e.app.Test(request, -1) if err != nil { t.Fatalf("执行 HTTP 请求失败:%v", err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { t.Fatalf("读取 HTTP 响应失败:%v", err) } return response.StatusCode, body } func uniqueTestID() uint { return uint(time.Now().UnixNano() % 1_000_000_000) }