test: 完善方案一业务逻辑测试和方案二规模测试
方案一(业务逻辑正确性测试): - 修复 SocialAccount.CreatedAt/UpdatedAt NULL 扫描问题(改为 *time.Time 指针) - 修复 OPLOG_003~006 数据隔离(改用唯一前缀+Search方法隔离) - 修复 DEV_008 设备列表测试(改用UserID过滤器隔离) - 修复并发测试 cache=private → cache=shared(SQLite连接共享) - 新增 testEnv 隔离架构(独立DB + 独立 httptest.Server) 方案二(真实数据规模测试): - 新增 LatencyStats P99/P95 百分位统计采集器 - 全部 16 个测试迁移至 newIsolatedDB(独立内存DB,WAL模式) - 关键查询添加 P99 多次采样统计(UL/LL/DV/DS/PR/AUTH/OPLOG) - 新增 CONC_SCALE_001~003 并发压测(50-100 goroutine) - 删除旧 setupScaleTestDB 死代码 - 双阈值体系:SQLite本地宽松阈值 vs PostgreSQL生产严格目标 共计 84 测试通过(68 业务逻辑 + 16 规模测试)
This commit is contained in:
@@ -20,8 +20,8 @@ type SocialAccount struct {
|
|||||||
Phone string `gorm:"type:varchar(20)" json:"phone,omitempty"`
|
Phone string `gorm:"type:varchar(20)" json:"phone,omitempty"`
|
||||||
Extra ExtraData `gorm:"type:text" json:"extra,omitempty"`
|
Extra ExtraData `gorm:"type:text" json:"extra,omitempty"`
|
||||||
Status SocialAccountStatus `gorm:"default:1" json:"status"`
|
Status SocialAccountStatus `gorm:"default:1" json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt *time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (SocialAccount) TableName() string {
|
func (SocialAccount) TableName() string {
|
||||||
@@ -63,16 +63,17 @@ type SocialAccountInfo struct {
|
|||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Status SocialAccountStatus `json:"status"`
|
Status SocialAccountStatus `json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialAccount) ToInfo() *SocialAccountInfo {
|
func (s *SocialAccount) ToInfo() *SocialAccountInfo {
|
||||||
|
createdAt := s.CreatedAt
|
||||||
return &SocialAccountInfo{
|
return &SocialAccountInfo{
|
||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
Provider: s.Provider,
|
Provider: s.Provider,
|
||||||
Nickname: s.Nickname,
|
Nickname: s.Nickname,
|
||||||
Avatar: s.Avatar,
|
Avatar: s.Avatar,
|
||||||
Status: s.Status,
|
Status: s.Status,
|
||||||
CreatedAt: s.CreatedAt,
|
CreatedAt: createdAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
83
internal/pagination/cursor.go
Normal file
83
internal/pagination/cursor.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// Package pagination provides cursor-based (keyset) pagination utilities.
|
||||||
|
//
|
||||||
|
// Unlike offset-based pagination (OFFSET/LIMIT), cursor pagination uses
|
||||||
|
// a composite key (typically created_at + id) to locate the "position" in
|
||||||
|
// the result set, giving O(limit) performance regardless of how deep you page.
|
||||||
|
package pagination
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cursor represents an opaque position in a sorted result set.
|
||||||
|
// It is serialized as a URL-safe base64 string for transport.
|
||||||
|
type Cursor struct {
|
||||||
|
// LastID is the primary key of the last item on the current page.
|
||||||
|
LastID int64 `json:"last_id"`
|
||||||
|
// LastValue is the sort column value of the last item (e.g. created_at).
|
||||||
|
LastValue time.Time `json:"last_value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode serializes a Cursor to a URL-safe base64 string suitable for query params.
|
||||||
|
func (c *Cursor) Encode() string {
|
||||||
|
if c == nil || c.LastID == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(c)
|
||||||
|
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode parses a base64-encoded cursor string back into a Cursor.
|
||||||
|
// Returns nil for empty strings (meaning "first page").
|
||||||
|
func Decode(encoded string) (*Cursor, error) {
|
||||||
|
if encoded == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid cursor encoding: %w", err)
|
||||||
|
}
|
||||||
|
var c Cursor
|
||||||
|
if err := json.Unmarshal(data, &c); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid cursor data: %w", err)
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageResult wraps a paginated response with cursor navigation info.
|
||||||
|
type PageResult[T any] struct {
|
||||||
|
Items []T `json:"items"`
|
||||||
|
Total int64 `json:"total"` // Approximate or exact total (optional for pure cursor mode)
|
||||||
|
NextCursor string `json:"next_cursor"` // Empty means no more pages
|
||||||
|
HasMore bool `json:"has_more"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultPageSize is the default number of items per page.
|
||||||
|
const DefaultPageSize = 20
|
||||||
|
|
||||||
|
// MaxPageSize caps the maximum allowed items per request to prevent abuse.
|
||||||
|
const MaxPageSize = 100
|
||||||
|
|
||||||
|
// ClampPageSize ensures size is within [1, MaxPageSize], falling back to DefaultPageSize.
|
||||||
|
func ClampPageSize(size int) int {
|
||||||
|
if size <= 0 {
|
||||||
|
return DefaultPageSize
|
||||||
|
}
|
||||||
|
if size > MaxPageSize {
|
||||||
|
return MaxPageSize
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildNextCursor creates a cursor from the last item's ID and timestamp.
|
||||||
|
// Returns empty string if there are no items.
|
||||||
|
func BuildNextCursor(lastID int64, lastTime time.Time) string {
|
||||||
|
if lastID == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return (&Cursor{LastID: lastID, LastValue: lastTime}).Encode()
|
||||||
|
}
|
||||||
2897
internal/service/business_logic_test.go
Normal file
2897
internal/service/business_logic_test.go
Normal file
File diff suppressed because it is too large
Load Diff
1787
internal/service/scale_test.go
Normal file
1787
internal/service/scale_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user