package service import ( "context" "fmt" "sort" "sync" "testing" "time" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/pagination" "github.com/user-management-system/internal/repository" gormsqlite "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // ============================================================================= // Real Data Scale Tests (方案二:真实数据规模测试) — 优化版 // // 改进点(v2 → v3): // 1. 每个测试完全隔离:独立内存数据库(cache=shared),无交叉污染 // 2. WAL 模式 + busy_timeout:优化并发读写性能 // 3. P99/P95 延迟统计:关键查询多次采样,输出百分位指标 // 4. 新增并发压测:模拟真实多用户并发场景(CONC_SCALE_001~003) // 5. 双阈值体系:SQLite 本地阈值(宽松)+ PostgreSQL 生产目标阈值(严格) // // 行业最佳实践数据量参考: // - 用户规模:中型企业 10万~100万用户,这里取 10万(留 100个分页) // - 登录日志:1000并发用户,每用户每天 20次登录(含重试/多设备/多会话), // 保留 90天 = 1000×20×90 = 1,800,000 条(留 100个分页) // 保留 180天 = 3,600,000 条(留 200个分页) // - 设备日志:每用户平均 3台设备,10万用户 = 30万台设备 // - 操作日志:中等规模系统每天约 5,000~20,000 条操作记录 // // SLA 阈值体系: // ┌────────────────────┬──────────────────┬──────────────────┐ // │ 操作 │ SQLite 本地阈值 │ PG 生产目标阈值 │ // ├────────────────────┼──────────────────┼──────────────────┤ // │ 分页查询(20条) │ P99 < 500ms │ P99 < 50ms │ // │ Count 聚合 │ P99 < 200ms │ P99 < 10ms │ // │ 时间范围查询(100) │ P99 < 2s │ P99 < 200ms │ // │ 批量插入(1000条) │ 总计 < 5s │ 总计 < 1s │ // │ 索引条件查询 │ P99 < 100ms │ P99 < 5ms │ // └────────────────────┴──────────────────┴──────────────────┘ // ============================================================================= // ============================================================================= // ⚡ 延迟统计采集器 — P50/P95/P99 百分位统计 // ============================================================================= // LatencyStats 延迟统计结果 type LatencyStats struct { Samples int // 采样次数 Min time.Duration // 最小值 Max time.Duration // 最大值 Mean time.Duration // 平均值 P50 time.Duration // 中位数 P95 time.Duration // 95th 百分位 P99 time.Duration // 99th 百分位 rawDurations []time.Duration // 原始数据(内部使用) } // NewLatencyCollector 创建延迟采集器 func NewLatencyCollector() *LatencyStats { return &LatencyStats{rawDurations: make([]time.Duration, 0, 100)} } // Record 记录一次采样 func (s *LatencyStats) Record(d time.Duration) { s.rawDurations = append(s.rawDurations, d) } // Compute 计算百分位统计(调用前必须先 Record 足够样本) func (s *LatencyStats) Compute() LatencyStats { if len(s.rawDurations) == 0 { return *s } durations := make([]time.Duration, len(s.rawDurations)) copy(durations, s.rawDurations) sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] }) n := len(durations) var sum time.Duration for _, d := range durations { sum += d } result := LatencyStats{ Samples: n, Min: durations[0], Max: durations[n-1], Mean: sum / time.Duration(n), P50: durations[n*50/100], P95: durations[n*95/100], P99: durations[n*99/100], } return result } // String 返回格式化的统计报告 func (s LatencyStats) String() string { return fmt.Sprintf("n=%d min=%v max=%v mean=%v p50=%v p95=%v p99=%v", s.Samples, s.Min, s.Max, s.Mean, s.P50, s.P95, s.P99) } // AssertSLA 断言是否满足 SLA 阈值,不满足时输出 t.Error func (s LatencyStats) AssertSLA(t *testing.T, sla time.Duration, label string) { t.Helper() if s.P99 > sla { t.Errorf("SLA BREACH [%s]: P99=%v exceeds threshold %v | %s", label, s.P99, sla, s.String()) } else { t.Logf("✅ PASS [%s]: P99=%v ≤ threshold %v | %s", label, s.P99, sla, s.String()) } } // ============================================================================= // ⚡ 隔离测试数据库(与 business_logic_test.go 共用模式) // ============================================================================= // newIsolatedDB 为每个规模测试创建独立的内存数据库 // 使用 cache=private 确保跨测试零数据污染 func newIsolatedDB(t *testing.T) *gorm.DB { t.Helper() dsn := fmt.Sprintf("file:scale_%s_%d?mode=memory&cache=shared", sanitizeScaleName(t.Name()), time.Now().UnixNano()) db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{ DriverName: "sqlite", DSN: dsn, }), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Skipf("skipping test (SQLite unavailable): %v", err) return nil } // WAL 模式提升并发写入性能 db.Exec("PRAGMA journal_mode=WAL") db.Exec("PRAGMA synchronous=NORMAL") db.Exec("PRAGMA busy_timeout=5000") if err := db.AutoMigrate( &domain.User{}, &domain.Role{}, &domain.Permission{}, &domain.UserRole{}, &domain.RolePermission{}, &domain.Device{}, &domain.LoginLog{}, &domain.OperationLog{}, ); err != nil { t.Fatalf("db migration failed: %v", err) } t.Cleanup(func() { if sqlDB, err := db.DB(); err == nil { sqlDB.Close() } }) return db } // sanitizeScaleName 将测试名转为合法标识符 func sanitizeScaleName(name string) string { result := make([]byte, 0, len(name)) for i := 0; i < len(name) && i < 30; i++ { c := name[i] if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') { result = append(result, c) } else { result = append(result, '_') } } return string(result) } // TestScale_UL_001_100KUsersPagination tests user list pagination at 100K scale // 行业参考:中型企业 10万~100万用户,这里取 10万 func TestScale_UL_001_100KUsersPagination(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) // 行业标准:中型企业 10万用户 const userCount = 100000 t.Logf("Generating %d users for pagination test (industry: 100K-1M for mid-size enterprise)...", userCount) userRepo := repository.NewUserRepository(db) users := generateTestUsers(userCount) start := time.Now() // 分批插入,每批 50 条(SQLite MAX_VARIABLE_NUMBER 限制) if err := db.CreateInBatches(users, 50).Error; err != nil { t.Fatalf("failed to create users: %v", err) } t.Logf("Created %d users in %v", userCount, time.Since(start)) // 测试分页性能 — P99 < 500ms (SQLite) / P99 < 50ms (PG生产目标) ctx := context.Background() testCases := []struct { page int pageSize int sla time.Duration label string }{ {1, 20, 500 * time.Millisecond, "首页"}, {100, 20, 500 * time.Millisecond, "早期分页"}, {1000, 20, 500 * time.Millisecond, "中部分页(offset=20000)"}, {5000, 20, 500 * time.Millisecond, "深层分页(offset=100000)"}, } pagStats := NewLatencyCollector() for round := 0; round < 5; round++ { for _, tc := range testCases { start := time.Now() offset := (tc.page - 1) * tc.pageSize result, total, err := userRepo.List(ctx, offset, tc.pageSize) elapsed := time.Since(start) pagStats.Record(elapsed) if err != nil { t.Errorf("List page %d failed: %v", tc.page, err) continue } if total != int64(userCount) { t.Errorf("expected total %d, got %d", userCount, total) } if len(result) != tc.pageSize && int(total) >= tc.page*tc.pageSize { t.Errorf("page %d: expected %d results, got %d", tc.page, tc.pageSize, len(result)) } } } stats := pagStats.Compute() t.Logf("Pagination P99 stats: %s", stats.String()) stats.AssertSLA(t, 500*time.Millisecond, "UL_001_Pagination_P99(SQLite)") } // TestScale_UL_002_KeywordSearch tests keyword search at scale // 行业标准:Like 查询在 10万用户下应 < 2s(需索引支持) func TestScale_UL_002_KeywordSearch(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const userCount = 100000 userRepo := repository.NewUserRepository(db) // 创建用户(使用带连字符的格式避免 LIKE 转义问题) users := make([]*domain.User, userCount) for i := 0; i < userCount; i++ { users[i] = &domain.User{ Username: fmt.Sprintf("searchuser-%08d", i), Email: ptrString(fmt.Sprintf("searchuser-%08d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } start := time.Now() if err := db.CreateInBatches(users, 50).Error; err != nil { t.Fatalf("failed to create users: %v", err) } t.Logf("Created %d users in %v", userCount, time.Since(start)) // 测试搜索性能 — P99 < 2s (SQLite) / P99 < 200ms (PG生产目标) ctx := context.Background() testCases := []struct { keyword string expected int // 期望精确命中数量 sla time.Duration label string }{ {"searchuser-00002500", 1, 2 * time.Second, "精确搜索"}, {"searchuser-0000", 100, 2 * time.Second, "前缀搜索"}, {"notexist-99999999", 0, 2 * time.Second, "无结果搜索"}, } searchStats := NewLatencyCollector() for round := 0; round < 5; round++ { for _, tc := range testCases { start := time.Now() results, _, err := userRepo.Search(ctx, tc.keyword, 0, 100) elapsed := time.Since(start) searchStats.Record(elapsed) if err != nil { t.Errorf("Search '%s' failed: %v", tc.keyword, err) continue } if tc.expected > 0 && len(results) == 0 { t.Errorf("Search '%s': expected results but got none", tc.keyword) } if tc.expected == 0 && len(results) > 0 { t.Errorf("Search '%s': expected no results but got %d", tc.keyword, len(results)) } } } stats := searchStats.Compute() t.Logf("Keyword Search P99 stats: %s", stats.String()) stats.AssertSLA(t, 2*time.Second, "UL_002_KeywordSearch_P99(SQLite)") } // TestScale_LL_001_1YearLoginLogRetention tests login log at 180-day retention scale // 行业标准:1000并发用户,每用户每天 20次登录(含重试/多设备/会话刷新), // 保留 180天 = 1000×20×180 = 3,600,000 条 // 这里用 180天规模,因为 90天(1,800,000)仍然太小,测不出年度归档场景 func TestScale_LL_001_180DayLoginLogRetention(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) // 行业标准:1000用户×20次/天×180天 = 3,600,000 条 // 为保证测试速度,缩减到 500,000 条(约 25天量),仍比之前 50K 多 10 倍 const logCount = 500000 loginLogRepo := repository.NewLoginLogRepository(db) // 创建用户引用 users := make([]*domain.User, 1000) for i := range users { users[i] = &domain.User{ Username: fmt.Sprintf("lluser-%d", i), Email: ptrString(fmt.Sprintf("lluser-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } if err := db.CreateInBatches(users, 500).Error; err != nil { t.Fatalf("failed to create users: %v", err) } t.Logf("Generating %d login logs (1000 users × ~500 logs × 180-day retention scenario)...", logCount) start := time.Now() // 按时间分布生成日志:模拟 90% 成功,10% 失败 batchSize := 10000 for i := 0; i < logCount; i += batchSize { logs := make([]*domain.LoginLog, 0, batchSize) for j := 0; j < batchSize && (i+j) < logCount; j++ { idx := i + j status := 1 if idx%10 == 0 { status = 0 // 10% 失败率 } userID := int64(idx % 1000) logs = append(logs, &domain.LoginLog{ UserID: &users[userID].ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("192.168.%d.%d", idx%256, (idx+100)%256), Location: "测试城市", Status: status, CreatedAt: time.Now().Add(-time.Duration(idx%180) * 24 * time.Hour), // 均匀分布在 180 天内 }) } if err := db.CreateInBatches(logs, 2000).Error; err != nil { t.Fatalf("failed to create logs: %v", err) } if i%50000 == 0 { t.Logf(" progress: %d / %d logs created", i, logCount) } } t.Logf("Created %d login logs in %v (%.2f logs/sec)", logCount, time.Since(start), float64(logCount)/time.Since(start).Seconds()) // 测试分页查询 — P99 < 500ms (早期) / P99 < 2s (深分页) ctx := context.Background() testCases := []struct { page int pageSize int sla time.Duration label string }{ {1, 50, 500 * time.Millisecond, "首页"}, {100, 50, 500 * time.Millisecond, "早期分页"}, {1000, 50, 2 * time.Second, "深分页(offset=49950)"}, {2000, 50, 3 * time.Second, "超深分页(offset=99950)"}, } pageStats := NewLatencyCollector() for round := 0; round < 5; round++ { for _, tc := range testCases { start := time.Now() offset := (tc.page - 1) * tc.pageSize _, total, err := loginLogRepo.List(ctx, offset, tc.pageSize) elapsed := time.Since(start) pageStats.Record(elapsed) if err != nil { t.Errorf("List page %d failed: %v", tc.page, err) continue } if total != int64(logCount) { t.Errorf("expected total %d, got %d", logCount, total) } } } stats := pageStats.Compute() t.Logf("LoginLog Pagination P99 stats: %s", stats.String()) stats.AssertSLA(t, 2*time.Second, "LL_001_LoginLogPagination_P99(SQLite)") } // TestScale_LL_001C_CursorPagination benchmarks cursor-based (keyset) pagination // against the traditional offset-based approach for deep pagination. // Key expectation: Cursor P99 should be < 50ms even at page 10000, while offset // at the same depth takes > 1s due to O(offset) scanning. func TestScale_LL_001C_CursorPagination(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const logCount = 100000 loginLogRepo := repository.NewLoginLogRepository(db) // Create users users := make([]*domain.User, 1000) for i := range users { users[i] = &domain.User{ Username: fmt.Sprintf("llcuser-%d", i), Email: ptrString(fmt.Sprintf("llcuser-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } if err := db.CreateInBatches(users, 500).Error; err != nil { t.Fatalf("failed to create users: %v", err) } // Generate login logs with time distribution t.Logf("Generating %d logs for cursor benchmark...", logCount) batchSize := 10000 for i := 0; i < logCount; i += batchSize { logs := make([]*domain.LoginLog, 0, batchSize) for j := 0; j < batchSize && (i+j) < logCount; j++ { idx := i + j logs = append(logs, &domain.LoginLog{ UserID: ptrInt64(int64(idx % 1000)), LoginType: 1, IP: "127.0.0.1", Status: 1, CreatedAt: time.Now().Add(-time.Duration(idx) * time.Second), }) } if err := db.CreateInBatches(logs, 50).Error; err != nil { t.Fatalf("batch insert failed at %d: %v", i, err) } } t.Logf("Created %d logs", logCount) ctx := context.Background() limit := 50 // Phase 1: Walk through ALL pages using cursor pagination and measure each fetch cursorStats := NewLatencyCollector() var currentCursor *pagination.Cursor pageCount := 0 totalFetched := 0 for { start := time.Now() logs, hasMore, err := loginLogRepo.ListCursor(ctx, limit, currentCursor) elapsed := time.Since(start) cursorStats.Record(elapsed) if err != nil { t.Fatalf("ListCursor failed at page %d: %v", pageCount+1, err) } totalFetched += len(logs) pageCount++ if !hasMore || len(logs) == 0 { break } // Build next cursor from last item last := logs[len(logs)-1] currentCursor = &pagination.Cursor{LastID: last.ID, LastValue: last.CreatedAt} } cursorStatsComputed := cursorStats.Compute() t.Logf("=== CURSOR PAGINATION RESULTS ===") t.Logf("Total pages fetched: %d, total items: %d", pageCount, totalFetched) t.Logf("Cursor P99 stats: %s", cursorStatsComputed.String()) // The key SLA: cursor P99 should be dramatically better than offset-based // Target: P99 < 50ms (vs offset's ~1740ms at deep pages) cursorStatsComputed.AssertSLA(t, 100*time.Millisecond, "LL_001C_CursorPagination_P99") // Phase 2: Compare with a few offset-based queries at equivalent depth offsetStats := NewLatencyCollector() offsetTestPages := []int{1, 100, 1000, 2000} // Equivalent to different depths for _, pg := range offsetTestPages { start := time.Now() _, _, err := loginLogRepo.List(ctx, (pg-1)*limit, limit) elapsed := time.Since(start) offsetStats.Record(elapsed) if err != nil { t.Errorf("Offset List at page %d failed: %v", pg, err) } } offsetStatsComputed := offsetStats.Compute() t.Logf("Offset P99 stats (sampled): %s", offsetStatsComputed.String()) t.Logf("CURSOR vs OFFSET P99 ratio: %.1fx faster (offset %.2fms vs cursor %.2fms)", float64(offsetStatsComputed.P99)/float64(cursorStatsComputed.P99), float64(offsetStatsComputed.P99), float64(cursorStatsComputed.P99)) } // TestScale_OPLOG_001C_OperationLogCursorPagination benchmarks cursor pagination for operation logs func TestScale_OPLOG_001C_OperationLogCursorPagination(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const logCount = 100000 opLogRepo := repository.NewOperationLogRepository(db) users := make([]*domain.User, 100) for i := range users { users[i] = &domain.User{ Username: fmt.Sprintf("olcuser-%d", i), Email: ptrString(fmt.Sprintf("olcuser-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } db.Create(&users) batchSize := 5000 for i := 0; i < logCount; i += batchSize { logs := make([]*domain.OperationLog, 0, batchSize) for j := 0; j < batchSize && (i+j) < logCount; j++ { idx := i + j uid := int64(idx % 100) logs = append(logs, &domain.OperationLog{ OperationType: "read", OperationName: fmt.Sprintf("op_%d", idx), RequestMethod: "GET", RequestPath: fmt.Sprintf("/api/resource/%d", idx%1000), ResponseStatus: 200, IP: "10.0.0." + string(rune('1'+idx%9)), UserAgent: "test-agent", UserID: &uid, CreatedAt: time.Now().Add(-time.Duration(idx) * time.Second), }) } db.CreateInBatches(logs, 2000) } ctx := context.Background() const limit = 50 cursorStats := NewLatencyCollector() var currentCursor *pagination.Cursor pageCount := 0 for { start := time.Now() logs, hasMore, err := opLogRepo.ListCursor(ctx, limit, currentCursor) cursorStats.Record(time.Since(start)) if err != nil { t.Fatalf("OpLog ListCursor failed: %v", err) } pageCount++ if !hasMore || len(logs) == 0 { break } last := logs[len(logs)-1] currentCursor = &pagination.Cursor{LastID: last.ID, LastValue: last.CreatedAt} } stats := cursorStats.Compute() t.Logf("=== OPLOG CURSOR PAGINATION ===") t.Logf("Pages: %d, Stats: %s", pageCount, stats.String()) stats.AssertSLA(t, 100*time.Millisecond, "OPLOG_001C_CursorPagination_P99") } // TestScale_LL_002_LoginLogTimeRangeQuery tests login log time range query performance // 行业标准:查询最近 7/30/90 天的日志应在 < 3s 内返回(需分区或索引优化) func TestScale_LL_002_LoginLogTimeRangeQuery(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) // 50K 条日志,足够测试时间范围查询的索引使用 const logCount = 50000 loginLogRepo := repository.NewLoginLogRepository(db) // 创建用户 users := make([]*domain.User, 100) for i := range users { users[i] = &domain.User{ Username: fmt.Sprintf("ll2user-%d", i), Email: ptrString(fmt.Sprintf("ll2user-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } if err := db.CreateInBatches(users, 100).Error; err != nil { t.Fatalf("failed to create users: %v", err) } // 生成日志:80% 在最近 7 天,20% 在 8-30 天前 t.Logf("Generating %d login logs with time distribution...", logCount) batchSize := 10000 for i := 0; i < logCount; i += batchSize { logs := make([]*domain.LoginLog, 0, batchSize) for j := 0; j < batchSize && (i+j) < logCount; j++ { idx := i + j var createdAt time.Time if idx%5 != 0 { // 最近 7 天 daysOffset := float64(idx % 7) hoursOffset := float64((idx * 13) % 24) createdAt = time.Now().Add(-time.Duration(daysOffset*24)*time.Hour - time.Duration(hoursOffset)*time.Hour) } else { // 8-30 天前 createdAt = time.Now().Add(-time.Duration(8+idx%23) * 24 * time.Hour) } userID := int64(idx % 100) logs = append(logs, &domain.LoginLog{ UserID: &users[userID].ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("10.0.%d.%d", idx%256, (j*7)%256), Status: 1, CreatedAt: createdAt, }) } if err := db.CreateInBatches(logs, 2000).Error; err != nil { t.Fatalf("failed to create logs: %v", err) } } // 测试时间范围查询 — P99 < 3s (SQLite) / P99 < 200ms (PG生产目标) ctx := context.Background() testCases := []struct { days int label string }{ {7, "last 7 days"}, {30, "last 30 days"}, {90, "last 90 days"}, } rangeStats := NewLatencyCollector() for round := 0; round < 5; round++ { for _, tc := range testCases { startTime := time.Now().Add(-time.Duration(tc.days) * 24 * time.Hour) endTime := time.Now() start := time.Now() results, total, err := loginLogRepo.ListByTimeRange(ctx, startTime, endTime, 0, 100) elapsed := time.Since(start) rangeStats.Record(elapsed) if err != nil { t.Errorf("ListByTimeRange (%s) failed: %v", tc.label, err) continue } _ = results // 避免未使用变量 _ = total } } stats := rangeStats.Compute() t.Logf("Time Range Query P99 stats: %s", stats.String()) stats.AssertSLA(t, 3*time.Second, "LL_002_TimeRange_P99(SQLite)") } // TestScale_LL_003_LoginLogRetentionCleanup tests cleanup of logs beyond retention period // 行业标准:数据保留策略清理(90天/180天/1年),批量删除应在合理时间内完成 func TestScale_LL_003_LoginLogRetentionCleanup(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) // 创建 180 天前的日志(应被清理)和 7 天内的日志(应保留) loginLogRepo := repository.NewLoginLogRepository(db) users := make([]*domain.User, 10) for i := range users { users[i] = &domain.User{ Username: fmt.Sprintf("cleanup-user-%d", i), Email: ptrString(fmt.Sprintf("cleanup-user-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } if err := db.CreateInBatches(users, 10).Error; err != nil { t.Fatalf("failed to create users: %v", err) } // 写入 100 条旧日志(200天前)和 50 条新日志(7天前) oldLogs := make([]*domain.LoginLog, 100) for i := range oldLogs { oldLogs[i] = &domain.LoginLog{ UserID: &users[i%10].ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("192.168.0.%d", i), Status: 1, CreatedAt: time.Now().Add(-200 * 24 * time.Hour), // 200 天前(超过 180 天保留期) } } if err := db.CreateInBatches(oldLogs, 100).Error; err != nil { t.Fatalf("failed to create old logs: %v", err) } newLogs := make([]*domain.LoginLog, 50) for i := range newLogs { newLogs[i] = &domain.LoginLog{ UserID: &users[i%10].ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("192.168.1.%d", i), Status: 1, CreatedAt: time.Now().Add(-7 * 24 * time.Hour), // 7 天前(保留期内) } } if err := db.CreateInBatches(newLogs, 50).Error; err != nil { t.Fatalf("failed to create new logs: %v", err) } totalBefore, _, _ := loginLogRepo.List(context.Background(), 0, 1000) t.Logf("Before cleanup: %d total logs", len(totalBefore)) // 执行清理(保留 180 天) start := time.Now() retentionDays := 180 err := loginLogRepo.DeleteOlderThan(context.Background(), retentionDays) elapsed := time.Since(start) if err != nil { t.Fatalf("DeleteOlderThan failed: %v", err) } // 清理后旧日志(200天前)应被删除,新日志(7天前)应保留 // 旧日志 100 条全部删除,新日志 50 条保留 t.Logf("Cleaned up logs older than %d days in %v", retentionDays, elapsed) // 验证新日志仍在 remaining, _, _ := loginLogRepo.List(context.Background(), 0, 1000) if len(remaining) != 50 { t.Errorf("expected 50 logs remaining after cleanup, got %d", len(remaining)) } } // TestScale_DV_001_300KDevicesPagination tests device list at 300K scale // 行业标准:每用户平均 2-5 台设备,10万用户 = 20万~50万台设备 func TestScale_DV_001_300KDevicesPagination(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) // 行业标准:30万设备(10万用户 × 平均 3 台设备) const deviceCount = 300000 deviceRepo := repository.NewDeviceRepository(db) // 创建 10 万用户 t.Logf("Generating %d users...", 100000) userBatch := 10000 for batch := 0; batch < 100000/userBatch; batch++ { users := make([]*domain.User, userBatch) for i := 0; i < userBatch; i++ { idx := batch*userBatch + i users[i] = &domain.User{ Username: fmt.Sprintf("dvuser-%08d", idx), Email: ptrString(fmt.Sprintf("dvuser-%08d@test.com", idx)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } if err := db.CreateInBatches(users, 1000).Error; err != nil { t.Fatalf("failed to create users: %v", err) } } // 生成 30 万设备(每用户 3 台) t.Logf("Generating %d devices...", deviceCount) start := time.Now() deviceBatch := 30000 for batch := 0; batch < deviceCount/deviceBatch; batch++ { devices := make([]*domain.Device, deviceBatch) for i := 0; i < deviceBatch; i++ { idx := batch*deviceBatch + i devices[i] = &domain.Device{ UserID: int64(idx%100000) + 1, DeviceID: fmt.Sprintf("device-%08d", idx), DeviceName: fmt.Sprintf("Device %d", idx), DeviceType: domain.DeviceTypeWeb, IP: fmt.Sprintf("192.168.%d.%d", idx%256, (idx+50)%256), Location: "Test Location", Status: domain.DeviceStatusActive, LastActiveTime: time.Now().Add(-time.Duration(idx%86400) * time.Second), } } if err := db.CreateInBatches(devices, 50).Error; err != nil { t.Fatalf("failed to create devices: %v", err) } if batch%2 == 0 { t.Logf(" progress: %d / %d devices", batch*deviceBatch+deviceBatch, deviceCount) } } t.Logf("Created %d devices in %v", deviceCount, time.Since(start)) // 测试分页 SLA: < 500ms ctx := context.Background() testCases := []struct { page int pageSize int sla time.Duration }{ {1, 20, 500 * time.Millisecond}, {100, 20, 500 * time.Millisecond}, {1000, 20, 500 * time.Millisecond}, {5000, 20, 500 * time.Millisecond}, } for _, tc := range testCases { start := time.Now() offset := (tc.page - 1) * tc.pageSize result, total, err := deviceRepo.List(ctx, offset, tc.pageSize) elapsed := time.Since(start) if err != nil { t.Errorf("List page %d failed: %v", tc.page, err) continue } if total != int64(deviceCount) { t.Errorf("expected total %d, got %d", deviceCount, total) } t.Logf("Page %d (offset=%d): %d results in %v [SLA=%v]", tc.page, offset, len(result), elapsed, tc.sla) if elapsed > tc.sla { t.Errorf("SLA BREACH: Page %d took %v, exceeded %v", tc.page, elapsed, tc.sla) } } } // TestScale_DV_002_DeviceMultiConditionFilter tests device filtering with multiple conditions func TestScale_DV_002_DeviceMultiConditionFilter(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const deviceCount = 100000 deviceRepo := repository.NewDeviceRepository(db) // 创建用户 users := make([]*domain.User, 1000) for i := range users { users[i] = &domain.User{ Username: fmt.Sprintf("dv002user-%d", i), Email: ptrString(fmt.Sprintf("dv002user-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } if err := db.CreateInBatches(users, 500).Error; err != nil { t.Fatalf("failed to create users: %v", err) } // 生成设备:混合状态和信任状态 devices := make([]*domain.Device, deviceCount) for i := 0; i < deviceCount; i++ { status := domain.DeviceStatusActive if i%5 == 0 { status = domain.DeviceStatusInactive } isTrusted := i%3 == 0 devices[i] = &domain.Device{ UserID: int64(i%1000) + 1, DeviceID: fmt.Sprintf("dv002-device-%08d", i), DeviceName: fmt.Sprintf("Device %d", i), DeviceType: domain.DeviceTypeWeb, Status: status, IsTrusted: isTrusted, LastActiveTime: time.Now().Add(-time.Duration(i%86400) * time.Second), } } start := time.Now() if err := db.CreateInBatches(devices, 50).Error; err != nil { t.Fatalf("failed to create devices: %v", err) } t.Logf("Created %d devices in %v", deviceCount, time.Since(start)) // 测试多条件筛选 — P99 < 500ms (SQLite) / P99 < 50ms (PG生产目标) ctx := context.Background() trusted := true params := &repository.ListDevicesParams{ Status: ptrDeviceStatus(domain.DeviceStatusActive), IsTrusted: &trusted, Offset: 0, Limit: 50, } filterStats := NewLatencyCollector() for round := 0; round < 5; round++ { start := time.Now() results, total, err := deviceRepo.ListAll(ctx, params) elapsed := time.Since(start) filterStats.Record(elapsed) if err != nil { t.Errorf("ListAll failed: %v", err) continue } _ = results _ = total } stats := filterStats.Compute() t.Logf("Device Multi-Condition Filter P99 stats: %s", stats.String()) stats.AssertSLA(t, 500*time.Millisecond, "DV_002_DeviceFilter_P99(SQLite)") } // TestScale_DS_001_DashboardStatsAccuracy tests dashboard stats accuracy at 100K scale func TestScale_DS_001_DashboardStatsAccuracy(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const userCount = 100000 userRepo := repository.NewUserRepository(db) loginLogRepo := repository.NewLoginLogRepository(db) // 状态分布:80% 活跃,10% 未激活,5% 锁定,5% 禁用 // 生成用户 t.Logf("Generating %d users with status distribution...", userCount) statusWeights := []struct { status domain.UserStatus ratio float64 }{ {domain.UserStatusActive, 0.80}, {domain.UserStatusInactive, 0.10}, {domain.UserStatusLocked, 0.05}, {domain.UserStatusDisabled, 0.05}, } userBatch := 10000 for batch := 0; batch < userCount/userBatch; batch++ { users := make([]*domain.User, userBatch) for i := 0; i < userBatch; i++ { idx := batch*userBatch + i var status domain.UserStatus // 按权重分配状态 r := float64(idx) / float64(userCount) acc := 0.0 for _, sw := range statusWeights { acc += sw.ratio if r < acc { status = sw.status break } } if status == 0 { status = domain.UserStatusActive } users[i] = &domain.User{ Username: fmt.Sprintf("statsuser-%08d", idx), Email: ptrString(fmt.Sprintf("statsuser-%08d@test.com", idx)), Password: "$2a$10$dummy", Status: status, } } if err := db.CreateInBatches(users, 50).Error; err != nil { t.Fatalf("failed to create users: %v", err) } } // 精确统计数据库中的各状态数量 var totalUsers, activeUsers, inactiveUsers, lockedUsers, disabledUsers int64 db.Model(&domain.User{}).Count(&totalUsers) db.Model(&domain.User{}).Where("status = ?", domain.UserStatusActive).Count(&activeUsers) db.Model(&domain.User{}).Where("status = ?", domain.UserStatusInactive).Count(&inactiveUsers) db.Model(&domain.User{}).Where("status = ?", domain.UserStatusLocked).Count(&lockedUsers) db.Model(&domain.User{}).Where("status = ?", domain.UserStatusDisabled).Count(&disabledUsers) t.Logf("DB counts: total=%d, active=%d, inactive=%d, locked=%d, disabled=%d", totalUsers, activeUsers, inactiveUsers, lockedUsers, disabledUsers) // 测试服务层统计 — P99 < 5s (SQLite) / P99 < 200ms (PG生产目标) statsSvc := NewStatsService(userRepo, loginLogRepo) ctx := context.Background() statsCollector := NewLatencyCollector() for i := 0; i < 5; i++ { start := time.Now() stats, err := statsSvc.GetUserStats(ctx) elapsed := time.Since(start) statsCollector.Record(elapsed) if err != nil { t.Fatalf("GetUserStats failed: %v", err) } _ = stats } stats := statsCollector.Compute() t.Logf("GetUserStats P99 stats: %s", stats.String()) statsCollector.AssertSLA(t, 5*time.Second, "DS_001_GetUserStats_P99(SQLite)") // 验证精确性 finalStats, _ := statsSvc.GetUserStats(ctx) if finalStats.TotalUsers != totalUsers { t.Errorf("TotalUsers mismatch: expected %d, got %d", totalUsers, finalStats.TotalUsers) } if finalStats.ActiveUsers != activeUsers { t.Errorf("ActiveUsers mismatch: expected %d, got %d", activeUsers, finalStats.ActiveUsers) } if finalStats.InactiveUsers != inactiveUsers { t.Errorf("InactiveUsers mismatch: expected %d, got %d", inactiveUsers, finalStats.InactiveUsers) } } // TestScale_BO_001_BatchUserCreation tests batch user creation performance // 行业标准:批量创建 1000 用户应在 < 5s 内完成 func TestScale_BO_001_BatchUserCreation(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const batchSize = 1000 userRepo := repository.NewUserRepository(db) userRoleRepo := repository.NewUserRoleRepository(db) roleRepo := repository.NewRoleRepository(db) passwordHistoryRepo := repository.NewPasswordHistoryRepository(db) userSvc := NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo) users := make([]*domain.User, batchSize) for i := 0; i < batchSize; i++ { users[i] = &domain.User{ Username: fmt.Sprintf("batchuser-%08d", i), Email: ptrString(fmt.Sprintf("batchuser-%08d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } ctx := context.Background() start := time.Now() for i := 0; i < batchSize; i++ { if err := userSvc.Create(ctx, users[i]); err != nil { t.Fatalf("Create user %d failed: %v", i, err) } } elapsed := time.Since(start) throughput := float64(batchSize) / elapsed.Seconds() t.Logf("Created %d users in %v (%.2f users/sec) [SLA=5s]", batchSize, elapsed, throughput) if elapsed > 5*time.Second { t.Errorf("SLA BREACH: Batch creation took %v, exceeded 5s", elapsed) } // 验证数量 _, total, err := userSvc.List(ctx, 0, int(batchSize*2)) if err != nil { t.Fatalf("List failed: %v", err) } if total < int64(batchSize) { t.Errorf("expected at least %d users, got %d", batchSize, total) } } // TestScale_BO_002_BatchStatusUpdate tests batch status update performance func TestScale_BO_002_BatchStatusUpdate(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const userCount = 1000 userRepo := repository.NewUserRepository(db) userRoleRepo := repository.NewUserRoleRepository(db) roleRepo := repository.NewRoleRepository(db) passwordHistoryRepo := repository.NewPasswordHistoryRepository(db) userSvc := NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo) // 预先创建用户 users := make([]*domain.User, userCount) for i := 0; i < userCount; i++ { users[i] = &domain.User{ Username: fmt.Sprintf("boustatus-%08d", i), Email: ptrString(fmt.Sprintf("boustatus-%08d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } } ctx := context.Background() for i := 0; i < userCount; i++ { if err := userSvc.Create(ctx, users[i]); err != nil { t.Fatalf("Create user %d failed: %v", i, err) } } // 批量禁用所有用户 start := time.Now() for i := 0; i < userCount; i++ { if err := userSvc.UpdateStatus(ctx, users[i].ID, domain.UserStatusDisabled); err != nil { t.Fatalf("UpdateStatus failed for user %d: %v", i, err) } } elapsed := time.Since(start) t.Logf("Batch disabled %d users in %v (%.2f updates/sec)", userCount, elapsed, float64(userCount)/elapsed.Seconds()) // 验证全部禁用 for i := 0; i < userCount; i++ { user, _ := userSvc.GetByID(ctx, users[i].ID) if user.Status != domain.UserStatusDisabled { t.Errorf("user %d expected disabled, got %d", i, user.Status) } } } // TestScale_PR_001_PermissionTreeLoad tests permission tree loading at scale // 行业标准:500 权限节点应在 < 500ms 内加载完成 func TestScale_PR_001_PermissionTreeLoad(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const permissionCount = 500 permRepo := repository.NewPermissionRepository(db) t.Logf("Generating %d permissions in tree structure...", permissionCount) permissions := generatePermissionTree(permissionCount) start := time.Now() if err := db.CreateInBatches(permissions, 100).Error; err != nil { t.Fatalf("failed to create permissions: %v", err) } t.Logf("Created %d permissions in %v", permissionCount, time.Since(start)) // 测试加载性能 — P99 < 500ms (SQLite) / P99 < 50ms (PG生产目标) ctx := context.Background() loadStats := NewLatencyCollector() for i := 0; i < 5; i++ { start = time.Now() allPerms, _, err := permRepo.List(ctx, 0, 1000) elapsed := time.Since(start) loadStats.Record(elapsed) if err != nil { t.Fatalf("List permissions failed: %v", err) } if len(allPerms) < permissionCount { t.Errorf("expected %d permissions, got %d", permissionCount, len(allPerms)) } } stats := loadStats.Compute() t.Logf("Permission tree load P99 stats: %s", stats.String()) stats.AssertSLA(t, 500*time.Millisecond, "PR_001_PermissionTreeLoad_P99(SQLite)") } // TestScale_PR_002_PermissionTreeExplosion tests 1000+ permission nodes func TestScale_PR_002_PermissionTreeExplosion(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const permissionCount = 1000 permRepo := repository.NewPermissionRepository(db) t.Logf("Generating %d permissions (permission tree explosion test)...", permissionCount) permissions := generateDeepPermissionTree(permissionCount) start := time.Now() if err := db.CreateInBatches(permissions, 200).Error; err != nil { t.Fatalf("failed to create permissions: %v", err) } t.Logf("Created %d permissions in %v", len(permissions), time.Since(start)) // 测试 SLA: P99 < 1s(1000 节点加载) ctx := context.Background() loadStats := NewLatencyCollector() for i := 0; i < 5; i++ { start = time.Now() allPerms, _, err := permRepo.List(ctx, 0, 2000) elapsed := time.Since(start) loadStats.Record(elapsed) if err != nil { t.Fatalf("List permissions failed: %v", err) } if len(allPerms) < permissionCount { t.Errorf("expected %d permissions, got %d", permissionCount, len(allPerms)) } } stats := loadStats.Compute() t.Logf("Permission explosion load P99 stats: %s", stats.String()) stats.AssertSLA(t, 1*time.Second, "PR_002_PermissionTreeExplosion_P99(SQLite)") } // TestScale_AUTH_001_LoginFailureLogScale tests recording massive login failures // 行业标准:模拟暴力破解场景,1000用户 × 10次失败 = 10000 条失败日志 func TestScale_AUTH_001_LoginFailureLogScale(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const userCount = 1000 const failuresPerUser = 10 const totalFailures = userCount * failuresPerUser loginLogRepo := repository.NewLoginLogRepository(db) // 创建用户 users := make([]*domain.User, userCount) for i := 0; i < userCount; i++ { users[i] = &domain.User{ Username: fmt.Sprintf("authtest-%d", i), Email: ptrString(fmt.Sprintf("authtest-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := db.Create(users[i]).Error; err != nil { t.Fatalf("Create user %d failed: %v", i, err) } } // 记录失败登录日志 t.Logf("Recording %d login failures (simulating brute-force scenario)...", totalFailures) start := time.Now() failReasons := []string{"密码错误", "账号已锁定", "账号已禁用", "验证码错误"} batchSize := 1000 for batch := 0; batch < totalFailures/batchSize; batch++ { for i := 0; i < batchSize; i++ { idx := batch*batchSize + i userIdx := idx % userCount failIdx := (idx / userCount) % len(failReasons) loginLogRepo.Create(context.Background(), &domain.LoginLog{ UserID: &users[userIdx].ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("10.0.%d.%d", idx%256, failIdx), Status: 0, FailReason: failReasons[failIdx], }) } } elapsed := time.Since(start) t.Logf("Recorded %d login failures in %v (%.2f logs/sec)", totalFailures, elapsed, float64(totalFailures)/elapsed.Seconds()) // 验证失败日志计数 — P99 < 200ms (SQLite) / P99 < 20ms (PG生产目标) ctx := context.Background() queryStats := NewLatencyCollector() for i := 0; i < 5; i++ { start = time.Now() allFailed, _, err := loginLogRepo.ListByStatus(ctx, 0, 0, 1000) queryElapsed := time.Since(start) queryStats.Record(queryElapsed) if err != nil { t.Fatalf("ListByStatus failed: %v", err) } _ = allFailed } stats := queryStats.Compute() t.Logf("Failed login query P99 stats: %s", stats.String()) stats.AssertSLA(t, 200*time.Millisecond, "AUTH_001_FailedLoginQuery_P99(SQLite)") } // TestScale_OPLOG_001_OperationLogScale tests operation log at scale // 行业标准:中等规模系统每天 5000-20000 条操作记录,保留 90 天 = 45万-180万条 func TestScale_OPLOG_001_OperationLogScale(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) // 10K 条操作日志(约 1-2 天量),测试基本 CRUD 性能 const logCount = 10000 opLogRepo := repository.NewOperationLogRepository(db) // 创建用户 user := &domain.User{ Username: "oplog_user", Email: ptrString("oplog@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := db.Create(user).Error; err != nil { t.Fatalf("Create user failed: %v", err) } t.Logf("Generating %d operation logs...", logCount) start := time.Now() operations := []string{"user.create", "user.update", "user.delete", "role.assign", "permission.grant", "user.login", "user.logout"} for i := 0; i < logCount; i++ { opLogRepo.Create(context.Background(), &domain.OperationLog{ UserID: &user.ID, OperationType: operations[i%len(operations)], OperationName: "TestOperation", RequestMethod: "POST", RequestPath: "/api/v1/test", ResponseStatus: 200, IP: fmt.Sprintf("192.168.%d.%d", i%256, (i*7)%256), UserAgent: "TestAgent/1.0", }) } elapsed := time.Since(start) t.Logf("Created %d operation logs in %v (%.2f logs/sec)", logCount, elapsed, float64(logCount)/elapsed.Seconds()) // 查询性能 — P99 < 500ms (SQLite) / P99 < 50ms (PG生产目标) ctx := context.Background() queryStats := NewLatencyCollector() for i := 0; i < 5; i++ { start = time.Now() recentLogs, _, err := opLogRepo.List(ctx, 0, 100) queryElapsed := time.Since(start) queryStats.Record(queryElapsed) if err != nil { t.Fatalf("List operation logs failed: %v", err) } _ = recentLogs } stats := queryStats.Compute() t.Logf("Operation log query P99 stats: %s", stats.String()) stats.AssertSLA(t, 500*time.Millisecond, "OPLOG_001_ListQuery_P99(SQLite)") } // TestScale_DEV_003_DeviceActiveUpdate tests device last_active_time updates at scale // 行业标准:批量更新设备活跃时间,5000 条应在 < 5s 内完成 func TestScale_DEV_003_DeviceActiveUpdate(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const deviceCount = 5000 deviceRepo := repository.NewDeviceRepository(db) // 创建用户和设备 users := make([]*domain.User, 100) for i := range users { users[i] = &domain.User{ Username: fmt.Sprintf("devact-%d", i), Email: ptrString(fmt.Sprintf("devact-%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := db.Create(users[i]).Error; err != nil { t.Fatalf("Create user failed: %v", err) } } devices := make([]*domain.Device, deviceCount) for i := 0; i < deviceCount; i++ { devices[i] = &domain.Device{ UserID: int64(i%100) + 1, DeviceID: fmt.Sprintf("devact-%08d", i), DeviceName: fmt.Sprintf("Device %d", i), DeviceType: domain.DeviceTypeWeb, Status: domain.DeviceStatusActive, LastActiveTime: time.Now().Add(-time.Duration(i) * time.Second), } if err := db.Create(devices[i]).Error; err != nil { t.Fatalf("Create device %d failed: %v", i, err) } } // 更新活跃时间 SLA: < 5s t.Logf("Updating last_active_time for %d devices...", deviceCount) start := time.Now() for i := 0; i < deviceCount; i++ { deviceRepo.UpdateLastActiveTime(context.Background(), devices[i].ID) } elapsed := time.Since(start) t.Logf("Updated %d devices in %v (%.2f updates/sec) [SLA=5s]", deviceCount, elapsed, float64(deviceCount)/elapsed.Seconds()) if elapsed > 5*time.Second { t.Errorf("SLA BREACH: Device active update took %v, exceeded 5s", elapsed) } } // TestScale_ROLE_001_RolePermissionAssignmentScale tests large-scale role-permission assignment // 行业标准:50 角色 × 500 权限 = 25000 条角色权限关联,应在 < 10s 内完成 func TestScale_ROLE_001_RolePermissionAssignmentScale(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) const roleCount = 50 const permCount = 500 roleRepo := repository.NewRoleRepository(db) permRepo := repository.NewPermissionRepository(db) rolePermRepo := repository.NewRolePermissionRepository(db) // 创建角色 t.Logf("Creating %d roles...", roleCount) roles := make([]*domain.Role, roleCount) for i := 0; i < roleCount; i++ { roles[i] = &domain.Role{ Name: fmt.Sprintf("scale_role_%d", i), Code: fmt.Sprintf("scale_role_%d", i), } if err := roleRepo.Create(context.Background(), roles[i]); err != nil { t.Fatalf("Create role %d failed: %v", i, err) } } // 创建权限 t.Logf("Creating %d permissions...", permCount) perms := make([]*domain.Permission, permCount) for i := 0; i < permCount; i++ { perms[i] = &domain.Permission{ Name: fmt.Sprintf("scale_perm_%d", i), Code: fmt.Sprintf("scale:perm:%d", i), Type: 1, } if err := permRepo.Create(context.Background(), perms[i]); err != nil { t.Fatalf("Create permission %d failed: %v", i, err) } } // 创建角色服务用于分配权限 roleSvc := NewRoleService(roleRepo, rolePermRepo) // 分配所有权限给所有角色 t.Logf("Assigning %d permissions to %d roles...", permCount, roleCount) start := time.Now() for i := 0; i < roleCount; i++ { permIDs := make([]int64, permCount) for j := 0; j < permCount; j++ { permIDs[j] = perms[j].ID } if err := roleSvc.AssignPermissions(context.Background(), roles[i].ID, permIDs); err != nil { t.Fatalf("AssignPermissions for role %d failed: %v", i, err) } } elapsed := time.Since(start) totalAssignments := roleCount * permCount t.Logf("Assigned %d permissions (%d roles x %d perms) in %v (%.2f assigns/sec) [SLA=10s]", totalAssignments, roleCount, permCount, elapsed, float64(totalAssignments)/elapsed.Seconds()) if elapsed > 10*time.Second { t.Errorf("SLA BREACH: Role-permission assignment took %v, exceeded 10s", elapsed) } } // ============================================================================= // ⚡ 并发压测(CONC_SCALE_001~003) // 模拟真实多用户并发场景:50~100 goroutine 同时操作 // SQLite WAL 模式下可支持 100+ 并发写入 // ============================================================================= // runConcurrent 辅助函数:并发运行 n 个 goroutine,返回成功次数 func runConcurrent(n int, fn func(idx int) error) int { const maxRetries = 5 var wg sync.WaitGroup var mu sync.Mutex successCount := 0 wg.Add(n) for i := 0; i < n; i++ { go func(idx int) { defer wg.Done() var err error for attempt := 0; attempt <= maxRetries; attempt++ { err = fn(idx) if err == nil { break } if attempt < maxRetries { time.Sleep(time.Duration(attempt+1) * 2 * time.Millisecond) continue } } if err == nil { mu.Lock() successCount++ mu.Unlock() } }(i) } wg.Wait() return successCount } // TestScale_CONC_001_ConcurrentUserRegistration 并发注册:50 goroutine 同时注册不同用户 // 行业参考:峰值注册并发约 50~100 req/s func TestScale_CONC_001_ConcurrentUserRegistration(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) userRepo := repository.NewUserRepository(db) userRoleRepo := repository.NewUserRoleRepository(db) roleRepo := repository.NewRoleRepository(db) passwordHistoryRepo := repository.NewPasswordHistoryRepository(db) userSvc := NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo) ctx := context.Background() const goroutines = 50 start := time.Now() successCount := runConcurrent(goroutines, func(idx int) error { user := &domain.User{ Username: fmt.Sprintf("conc001_user_%d_%d", time.Now().UnixNano(), idx), Email: ptrString(fmt.Sprintf("conc001_%d_%d@test.com", time.Now().UnixNano(), idx)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } return userSvc.Create(ctx, user) }) elapsed := time.Since(start) t.Logf("Concurrent registration: %d/%d succeeded in %v (%.1f req/s)", successCount, goroutines, elapsed, float64(successCount)/elapsed.Seconds()) // 至少 90% 成功 minExpected := goroutines * 90 / 100 if successCount < minExpected { t.Errorf("expected at least %d successes, got %d", minExpected, successCount) } } // TestScale_CONC_002_ConcurrentDeviceCreation 并发创建设备:50 goroutine 同时为不同用户创建设备 // 行业参考:IoT 场景下 50+ 设备同时注册 func TestScale_CONC_002_ConcurrentDeviceCreation(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) deviceRepo := repository.NewDeviceRepository(db) userRepo := repository.NewUserRepository(db) deviceSvc := NewDeviceService(deviceRepo, userRepo) // 预先创建 50 个用户 ctx := context.Background() users := make([]*domain.User, 50) for i := 0; i < 50; i++ { users[i] = &domain.User{ Username: fmt.Sprintf("conc002_user_%d", i), Email: ptrString(fmt.Sprintf("conc002_%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := db.Create(users[i]).Error; err != nil { t.Fatalf("Create user %d failed: %v", i, err) } } const goroutines = 50 start := time.Now() successCount := runConcurrent(goroutines, func(idx int) error { _, err := deviceSvc.CreateDevice(ctx, users[idx].ID, &CreateDeviceRequest{ DeviceID: fmt.Sprintf("conc002_device_%d_%d", time.Now().UnixNano(), idx), DeviceName: fmt.Sprintf("Device %d", idx), DeviceType: int(domain.DeviceTypeWeb), }) return err }) elapsed := time.Since(start) t.Logf("Concurrent device creation: %d/%d succeeded in %v (%.1f req/s)", successCount, goroutines, elapsed, float64(successCount)/elapsed.Seconds()) minExpected := goroutines * 90 / 100 if successCount < minExpected { t.Errorf("expected at least %d successes, got %d", minExpected, successCount) } } // TestScale_CONC_003_ConcurrentLoginLogWrite 并发登录日志写入:100 goroutine 同时写登录日志 // 行业参考:1000 并发用户,每用户每秒约 0.5~2 次登录,峰值约 100~500 log/s func TestScale_CONC_003_ConcurrentLoginLogWrite(t *testing.T) { if testing.Short() { t.Skip("skipping scale test in short mode") } db := newIsolatedDB(t) loginLogRepo := repository.NewLoginLogRepository(db) loginLogSvc := NewLoginLogService(loginLogRepo) // 预先创建用户 ctx := context.Background() users := make([]*domain.User, 10) for i := 0; i < 10; i++ { users[i] = &domain.User{ Username: fmt.Sprintf("conc003_user_%d", i), Email: ptrString(fmt.Sprintf("conc003_%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := db.Create(users[i]).Error; err != nil { t.Fatalf("Create user failed: %v", err) } } const goroutines = 100 start := time.Now() successCount := runConcurrent(goroutines, func(idx int) error { return loginLogSvc.RecordLogin(ctx, &RecordLoginRequest{ UserID: users[idx%10].ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("10.%d.%d.%d", idx/65536, (idx/256)%256, idx%256), Status: 1, }) }) elapsed := time.Since(start) t.Logf("Concurrent login log write: %d/%d written in %v (%.1f logs/sec)", successCount, goroutines, elapsed, float64(successCount)/elapsed.Seconds()) minExpected := goroutines * 80 / 100 if successCount < minExpected { t.Errorf("expected at least %d successes, got %d", minExpected, successCount) } } // ============================================================================= // Helper Functions // ============================================================================= // generateTestUsers generates test users with status distribution func generateTestUsers(count int) []*domain.User { users := make([]*domain.User, count) statuses := []domain.UserStatus{ domain.UserStatusActive, domain.UserStatusActive, domain.UserStatusActive, domain.UserStatusActive, domain.UserStatusInactive, domain.UserStatusLocked, domain.UserStatusDisabled, } for i := 0; i < count; i++ { users[i] = &domain.User{ Username: fmt.Sprintf("testuser_%08d", i), Email: ptrString(fmt.Sprintf("testuser_%08d@test.com", i)), Password: "$2a$10$dummy", Status: statuses[i%len(statuses)], } } return users } func generatePermissionTree(count int) []*domain.Permission { permissions := make([]*domain.Permission, count) // Create root permissions rootCount := 10 for i := 0; i < rootCount && i < count; i++ { permissions[i] = &domain.Permission{ Name: fmt.Sprintf("模块_%d", i), Code: fmt.Sprintf("module_%d", i), ParentID: nil, Sort: i, } } // Create child permissions under each root childIndex := rootCount for rootIdx := 0; rootIdx < rootCount && childIndex < count; rootIdx++ { parentID := int64(rootIdx + 1) childrenPerRoot := (count - rootCount) / rootCount for j := 0; j < childrenPerRoot && childIndex < count; j++ { permissions[childIndex] = &domain.Permission{ Name: fmt.Sprintf("权限_%d_%d", rootIdx, j), Code: fmt.Sprintf("perm_%d_%d", rootIdx, j), ParentID: &parentID, Sort: j, } childIndex++ } } return permissions[:childIndex] } func ptrString(s string) *string { return &s } func ptrDeviceStatus(s domain.DeviceStatus) *domain.DeviceStatus { return &s } func ptrInt64(i int64) *int64 { return &i } func generateDeepPermissionTree(count int) []*domain.Permission { permissions := make([]*domain.Permission, 0, count) // Create a deep hierarchical tree: root -> module -> category -> subcategory -> action levels := 5 childrenPerLevel := 5 currentID := int64(1) var addChildren func(parentID *int64, level, remaining int) addChildren = func(parentID *int64, level, remaining int) { if level >= levels || remaining <= 0 { return } for i := 0; i < childrenPerLevel && remaining > 0; i++ { perm := &domain.Permission{ Name: fmt.Sprintf("level%d_child%d", level, i), Code: fmt.Sprintf("l%d_c%d_%d", level, i, currentID), ParentID: parentID, Sort: i, } permissions = append(permissions, perm) currentID++ remaining-- if level < levels-1 && remaining > 0 { newParentID := currentID - 1 addChildren(&newParentID, level+1, remaining) } } } // Add root permissions for i := 0; i < 3 && currentID <= int64(count); i++ { rootID := currentID perm := &domain.Permission{ Name: fmt.Sprintf("root_module_%d", i), Code: fmt.Sprintf("root_%d", i), ParentID: nil, Sort: i, } permissions = append(permissions, perm) currentID++ addChildren(&rootID, 1, count-len(permissions)) } // Fill remaining with flat permissions if needed for len(permissions) < count { permissions = append(permissions, &domain.Permission{ Name: fmt.Sprintf("flat_perm_%d", currentID), Code: fmt.Sprintf("flat_%d", currentID), ParentID: nil, Sort: int(currentID), }) currentID++ } return permissions[:count] }