Files
CamTalk/docs/PLAN_USER_MODULE.md
hhs 4b30e67c2e feat: 扩展 Config 结构体,新增 AuthConfig 配置
- 新增 AuthConfig 结构体(JWTSecret, AccessTTL, RefreshTTL)
- 在 Config 中添加 Auth 字段
- 设置默认值:access_ttl=15分钟,refresh_ttl=10080分钟(7天)
- JWTSecret 必须通过环境变量 CAMTALK_AUTH_JWT_SECRET 设置
2026-06-14 16:40:06 +08:00

28 KiB
Raw Blame History

CamTalk 后端用户模块构建计划

Context

后端 AI 管道STT → LLM → TTS已完成现在需要实现用户系统和对话持久化。目标用户注册登录后,可在对话列表中选择历史对话继续交谈

设计文档:docs/11-持久化与用户系统设计.md(最高依据) 技术选型:docs/04-技术选型.md 第四章 现有后端计划:docs/PLAN_BACKEND.mdAI 管道部分已完成)

当前后端状态

模块 状态
config 已完成,需扩展 AuthConfig
logger 已完成
errors 已完成,需扩展用户相关错误码
models 已完成,需扩展 User/Session
session manager 内存/Redis 已完成,需扩展 user_id 绑定
AI 服务层 STT/LLM/TTS 已完成
orchestrator 已完成
ws handler 已完成,需接入 JWT 认证
REST API sessions CRUD 已完成,需新增 auth + conversations

新增依赖

用途 引入阶段
github.com/golang-jwt/jwt/v5 JWT 签发/校验 Phase 1
golang.org/x/crypto/bcrypt 密码哈希 Phase 1
github.com/jackc/pgx/v5 PostgreSQL 驱动 Phase 2

分阶段实施

Phase 1配置扩展 + 数据库连接

目标:扩展配置结构体,建立 PostgreSQL 连接池。

# 任务 文件 说明
1.1 扩展 Config 结构体 internal/config/config.go 新增 AuthConfigJWTSecret, AccessTTL, RefreshTTLStorageConfig 已有 Driver/DSN 字段
1.2 添加配置默认值 internal/config/config.go auth.access_ttl 默认 15auth.refresh_ttl 默认 10080
1.3 实现数据库连接池 internal/store/db.go NewPostgresPool(ctx, dsn) (*pgxpool.Pool, error),启动时调用,注入到各 repository
1.4 编写 schema 迁移脚本 migrations/001_users.up.sql users 表 + refresh_tokens
1.5 编写回滚脚本 migrations/001_users.down.sql DROP TABLE
1.6 main.go 条件初始化 DB cmd/server/main.go storage.driver == "postgres" 时创建 pgxpool否则跳过纯内存模式

配置扩展示例

// internal/config/config.go 新增

type AuthConfig struct {
    JWTSecret  string `mapstructure:"jwt_secret"`   // 必须通过 CAMTALK_AUTH_JWT_SECRET 设置
    AccessTTL  int    `mapstructure:"access_ttl"`   // 分钟,默认 15
    RefreshTTL int    `mapstructure:"refresh_ttl"`  // 分钟,默认 10080
}

数据库连接

// internal/store/db.go

package store

import (
    "context"
    "github.com/jackc/pgx/v5/pgxpool"
)

func NewPostgresPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
    cfg, err := pgxpool.ParseConfig(dsn)
    if err != nil {
        return nil, err
    }
    cfg.MaxConns = 10
    return pgxpool.NewWithConfig(ctx, cfg)
}

Phase 2用户模型 + Repository

目标:定义用户数据模型和持久化接口。

# 任务 文件 说明
2.1 扩展 models internal/models/models.go 新增 User 结构体ID, Username, PasswordHash, CreatedAt, UpdatedAt
2.2 定义 UserRepository 接口 internal/store/user.go Create, FindByUsername, FindByID, SaveRefreshToken, FindRefreshToken, DeleteRefreshToken
2.3 实现 PostgreSQL UserRepository internal/store/user_pg.go pgx 实现,所有方法使用 pgxpool.Pool
2.4 实现内存 UserRepository测试用 internal/store/user_mem.go sync.RWMutex + map单元测试时注入
2.5 编写 UserRepository 测试 internal/store/user_pg_test.go 需要测试 DB 或 mock

UserRepository 接口

// internal/store/user.go

package store

import (
    "context"
    "errors"
    "time"
)

var (
    ErrUserNotFound      = errors.New("user not found")
    ErrUsernameTaken     = errors.New("username already taken")
    ErrRefreshTokenNotFound = errors.New("refresh token not found")
)

type UserRepository interface {
    // Create 创建用户,返回生成的 ID。
    Create(ctx context.Context, username, passwordHash string) (string, error)

    // FindByUsername 按用户名查找,不存在返回 ErrUserNotFound。
    FindByUsername(ctx context.Context, username string) (*User, error)

    // FindByID 按 ID 查找,不存在返回 ErrUserNotFound。
    FindByID(ctx context.Context, id string) (*User, error)

    // SaveRefreshToken 保存 refresh token hash。
    SaveRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error

    // FindRefreshToken 按 token hash 查找,返回 user_id。不存在返回 ErrRefreshTokenNotFound。
    FindRefreshToken(ctx context.Context, tokenHash string) (string, error)

    // DeleteRefreshToken 按 token hash 删除。
    DeleteRefreshToken(ctx context.Context, tokenHash string) error

    // DeleteUserRefreshTokens 删除用户的所有 refresh token登出所有设备
    DeleteUserRefreshTokens(ctx context.Context, userID string) error
}

// User 用户数据模型store 层)。
type User struct {
    ID           string
    Username     string
    PasswordHash string
    CreatedAt    time.Time
    UpdatedAt    time.Time
}

Phase 3JWT + 认证服务

目标:实现 JWT 签发/校验、bcrypt 密码处理、认证业务逻辑。

# 任务 文件 说明
3.1 实现 TokenManager internal/auth/jwt.go GeneratePair, ValidateAccess, ValidateRefresh, HashToken
3.2 实现密码工具 internal/auth/password.go HashPassword(password) (string, error), CheckPassword(hash, password) error
3.3 实现 AuthMiddleware internal/auth/middleware.go Gin 中间件,从 Authorization: Bearer <token> 提取 Claims 写入 Context
3.4 定义 AuthService 接口 internal/auth/service.go 业务层封装:Register, Login, Refresh, Logout
3.5 实现 AuthService internal/auth/service.go 组合 TokenManager + UserRepository
3.6 编写 TokenManager 测试 internal/auth/jwt_test.go 生成/校验/过期/hash
3.7 编写 AuthService 测试 internal/auth/service_test.go mock UserRepository覆盖注册重复、密码错误、token 轮转

TokenManager 核心实现

// internal/auth/jwt.go

type Claims struct {
    UserID   string `json:"user_id"`
    Username string `json:"username"`
    jwt.RegisteredClaims
}

type TokenManager struct {
    secret     []byte
    accessTTL  time.Duration
    refreshTTL time.Duration
}

func NewTokenManager(secret string, accessTTL, refreshTTL time.Duration) *TokenManager {
    return &TokenManager{
        secret:     []byte(secret),
        accessTTL:  accessTTL,
        refreshTTL: refreshTTL,
    }
}

func (tm *TokenManager) GeneratePair(userID, username string) (access, refresh string, err error) {
    // access_token: 15min
    accessClaims := &Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(tm.accessTTL)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "camtalk",
        },
    }
    accessTkn := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
    access, err = accessTkn.SignedString(tm.secret)
    if err != nil {
        return "", "", err
    }

    // refresh_token: 7day, 含唯一 token_id
    tokenID := uuid.New().String()
    refreshClaims := &Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ID:        tokenID,
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(tm.refreshTTL)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "camtalk",
        },
    }
    refreshTkn := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
    refresh, err = refreshTkn.SignedString(tm.secret)
    return
}

func (tm *TokenManager) ValidateAccess(tokenStr string) (*Claims, error) {
    return tm.validate(tokenStr)
}

func (tm *TokenManager) ValidateRefresh(tokenStr string) (*Claims, error) {
    return tm.validate(tokenStr)
}

func (tm *TokenManager) validate(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) {
        return tm.secret, nil
    })
    if err != nil {
        return nil, err
    }
    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, jwt.ErrTokenInvalidClaims
    }
    return claims, nil
}

// HashToken SHA256 hash用于 DB 存储。
func HashToken(token string) string {
    h := sha256.Sum256([]byte(token))
    return hex.EncodeToString(h[:])
}

AuthService 接口

// internal/auth/service.go

type RegisterRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type LoginRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type RefreshRequest struct {
    RefreshToken string `json:"refresh_token"`
}

type AuthResponse struct {
    User         UserResponse `json:"user"`
    AccessToken  string       `json:"access_token"`
    RefreshToken string       `json:"refresh_token"`
}

type UserResponse struct {
    ID        string    `json:"id"`
    Username  string    `json:"username"`
    CreatedAt time.Time `json:"created_at"`
}

type Service interface {
    Register(ctx context.Context, req RegisterRequest) (*AuthResponse, error)
    Login(ctx context.Context, req LoginRequest) (*AuthResponse, error)
    Refresh(ctx context.Context, req RefreshRequest) (*AuthResponse, error)
    Logout(ctx context.Context, userID, refreshToken string) error
}

Phase 4认证 REST API

目标:实现注册、登录、刷新、登出四个端点。

# 任务 文件 说明
4.1 实现 AuthHandler internal/api/auth.go Register, Login, Refresh, Logout 处理函数
4.2 输入校验 internal/api/auth.go 用户名 3-64 字符,密码 8-72 字符
4.3 注册路由 internal/api/auth.go RegisterRoutes(rg *gin.RouterGroup)
4.4 main.go 接入 cmd/server/main.go 创建 TokenManager + AuthService + AuthHandler注册路由
4.5 编写 API 测试 internal/api/auth_test.go httptest + mock AuthService

AuthHandler 结构

// internal/api/auth.go

type AuthHandler struct {
    authService auth.Service
}

func NewAuthHandler(authService auth.Service) *AuthHandler {
    return &AuthHandler{authService: authService}
}

func (h *AuthHandler) RegisterRoutes(rg *gin.RouterGroup) {
    auth := rg.Group("/auth")
    {
        auth.POST("/register", h.Register)
        auth.POST("/login", h.Login)
        auth.POST("/refresh", h.Refresh)
        auth.POST("/logout", auth.AuthMiddleware(), h.Logout)
    }
}

错误响应格式(统一现有风格):

{
  "code": "USERNAME_TAKEN",
  "message": "username already taken"
}

新增错误码到 internal/errors/codes.go

const (
    CodeUsernameTaken      = "USERNAME_TAKEN"
    CodeInvalidCredentials = "INVALID_CREDENTIALS"
    CodeInvalidToken       = "INVALID_TOKEN"
    CodeInvalidInput       = "INVALID_INPUT"
)

Phase 5Session Manager 改造

目标Session Manager 关联 user_id支持对话列表查询。

# 任务 文件 说明
5.1 扩展 Session 模型 internal/models/models.go Session 新增 UserID, Title, UpdatedAt 字段
5.2 扩展 Manager 接口 internal/session/manager.go Create 签名加 userID;新增 ListByUser, UpdateTitle;新增 ConversationSummary 类型
5.3 修改 MemoryManager internal/session/memory.go Create 存储 userID实现 ListByUser(遍历+过滤+排序);实现 UpdateTitle
5.4 修改 RedisManager internal/session/redis.go session:{id}:meta 新增 user_idtitle 字段;ListByUser 使用 Redis Set user:{id}:sessions 索引
5.5 编写新方法测试 internal/session/memory_test.go 覆盖 ListByUser 分页、UpdateTitle、Create 带 userID
5.6 更新 ws handler 调用 internal/ws/handler.go sessionMgr.Create 调用传入 userID此时 Phase 7 才有真实 userID先用空字符串兼容

接口变更

// internal/session/manager.go 变更

type Manager interface {
    // Create 签名变更:新增 userID 参数
    Create(ctx context.Context, userID string, config models.SessionConfig) (string, error)

    // 新增方法
    ListByUser(ctx context.Context, userID string, page, size int) ([]ConversationSummary, int, error)
    UpdateTitle(ctx context.Context, sessionID string, title string) error

    // 其余方法不变...
}

type ConversationSummary struct {
    ID           string    `json:"id"`
    Title        string    `json:"title"`
    LastMessage  string    `json:"last_message"`
    MessageCount int       `json:"message_count"`
    UpdatedAt    time.Time `json:"updated_at"`
}

MemoryManager ListByUser 实现思路

func (m *MemoryManager) ListByUser(ctx context.Context, userID string, page, size int) ([]ConversationSummary, int, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()

    // 收集该用户的所有 session
    var list []ConversationSummary
    for _, entry := range m.sessions {
        if entry.session.UserID != userID {
            continue
        }
        summary := ConversationSummary{
            ID:           entry.session.ID,
            Title:        entry.session.Title,
            MessageCount: len(entry.history),
            UpdatedAt:    entry.lastActive,
        }
        if len(entry.history) > 0 {
            summary.LastMessage = entry.history[len(entry.history)-1].Content
        }
        list = append(list, summary)
    }

    // 按 UpdatedAt 降序排序
    sort.Slice(list, func(i, j int) bool {
        return list[i].UpdatedAt.After(list[j].UpdatedAt)
    })

    total := len(list)

    // 分页
    start := (page - 1) * size
    if start >= total {
        return []ConversationSummary{}, total, nil
    }
    end := start + size
    if end > total {
        end = total
    }

    return list[start:end], total, nil
}

Phase 6对话 REST API

目标:实现对话 CRUD 和历史消息查询端点。

# 任务 文件 说明
6.1 实现 ConversationHandler internal/api/conversation.go List, Create, Get, UpdateTitle, Delete, GetMessages
6.2 权限校验 internal/api/conversation.go 每个端点校验 session.UserID == claims.UserID
6.3 注册路由 internal/api/conversation.go RegisterRoutes(rg *gin.RouterGroup),全部走 AuthMiddleware
6.4 main.go 接入 cmd/server/main.go 创建 ConversationHandler 并注册
6.5 编写 API 测试 internal/api/conversation_test.go httptest + mock SessionManager

ConversationHandler 结构

// internal/api/conversation.go

type ConversationHandler struct {
    sessionMgr session.Manager
}

func (h *ConversationHandler) RegisterRoutes(rg *gin.RouterGroup) {
    conv := rg.Group("/conversations", auth.AuthMiddleware(tokenMgr))
    {
        conv.GET("", h.List)
        conv.POST("", h.Create)
        conv.GET("/:id", h.Get)
        conv.PATCH("/:id", h.UpdateTitle)
        conv.DELETE("/:id", h.Delete)
        conv.GET("/:id/messages", h.GetMessages)
    }
}

权限校验模式(每个端点复用):

func (h *ConversationHandler) getSessionForUser(c *gin.Context, sessionID string) (*models.Session, error) {
    sess, err := h.sessionMgr.Get(c.Request.Context(), sessionID)
    if err != nil {
        return nil, err
    }
    userID := c.GetString("user_id") // 从 AuthMiddleware 写入
    if sess.UserID != userID {
        return nil, session.ErrSessionNotFound // 返回 404 而非 403避免信息泄露
    }
    return sess, nil
}

GetMessages 实现要点

  • 从 Session Manager 的 GetHistory 获取消息
  • 支持 ?limit=50&before=<message_id> 分页
  • 内存实现中history 是全量存储的,直接按索引切片即可

Phase 7WebSocket 认证集成

目标WS 连接需要 JWT 认证,支持指定 conversation_id 恢复历史对话。

# 任务 文件 说明
7.1 修改 ServeWS 签名 internal/ws/handler.go 新增 tokenMgr *auth.TokenManager 参数
7.2 WS 连接认证 internal/ws/handler.go ?token=xxx 提取并校验 access_token失败返回 401
7.3 conversation_id 处理 internal/ws/handler.go ?conversation_id=xxx 存在时:校验归属 → LoadFromDB → 复用 session否则创建新 session
7.4 AppendMessage 自动标题 internal/session/memory.go 首条 user 消息时,如果 title == "新对话",自动更新为前 20 字符
7.5 main.go 更新 cmd/server/main.go 传入 tokenMgr 到 ServeWS
7.7 编写认证测试 internal/ws/handler_test.go 测试无 token / 过期 token / 有效 token / conversation_id 恢复

WS 连接流程变更

客户端请求:  GET /ws?token=<access>&conversation_id=<uuid>

服务端处理:
  1. token 为空 → 401 {"error": "missing token"}
  2. token 无效/过期 → 401 {"error": "invalid token"}
  3. conversation_id 非空:
     a. session 不存在或 user_id 不匹配 → 401 {"error": "SESSION_NOT_FOUND"}
     b. sessionMgr.LoadFromDB(conversationID) → 加载历史到热存储
     c. sessionID = conversationID
  4. conversation_id 为空:
     a. sessionMgr.Create(userID, defaultConfig) → 创建新 session
  5. Upgrade WebSocket → 发送 connected 消息

对话标题自动生成

// internal/session/memory.go — AppendMessage 中追加逻辑

func (m *MemoryManager) AppendMessage(ctx context.Context, sessionID string, msg models.Message) error {
    m.mu.Lock()
    defer m.mu.Unlock()

    entry, ok := m.sessions[sessionID]
    if !ok {
        return ErrSessionNotFound
    }

    entry.history = append(entry.history, msg)
    entry.lastActive = time.Now()

    // 自动更新标题
    if msg.Role == "user" && entry.session.Title == "新对话" {
        entry.session.Title = generateTitle(msg.Content)
    }

    // 限制历史上限
    if len(entry.history) > m.maxHistory {
        entry.history = entry.history[len(entry.history)-m.maxHistory:]
    }

    return nil
}

func generateTitle(firstMessage string) string {
    runes := []rune(firstMessage)
    if len(runes) > 20 {
        return string(runes[:20]) + "…"
    }
    return firstMessage
}

Phase 8消息持久化Write-Through

目标:对话消息同时写入 PostgreSQL保证重启不丢数据。

# 任务 文件 说明
8.1 定义 MessageRepository 接口 internal/store/message.go SaveMessage, GetMessages, GetLastMessage
8.2 实现 PostgreSQL MessageRepository internal/store/message_pg.go pgx 实现
8.3 Session Manager 注入 MessageRepository internal/session/memory.go AppendMessage 时同时调用 repo.SaveMessagewrite-through
8.4 LoadFromDB 实现 internal/session/memory.go 从 PostgreSQL 读取消息加载到内存 history
8.5 ConversationSummary 查询优化 internal/store/message_pg.go 对话列表的 last_message 和 message_count 通过 SQL 聚合查询

MessageRepository 接口

// internal/store/message.go

type MessageRepository interface {
    // SaveMessage 保存一条消息。
    SaveMessage(ctx context.Context, sessionID string, msg models.Message, tokensUsed int) error

    // GetMessages 获取会话的消息列表(分页,按 id 升序)。
    GetMessages(ctx context.Context, sessionID string, limit int, beforeID int64) ([]StoredMessage, error)

    // GetLastMessage 获取会话的最后一条消息。
    GetLastMessage(ctx context.Context, sessionID string) (*StoredMessage, error)

    // GetMessageCount 获取会话的消息总数。
    GetMessageCount(ctx context.Context, sessionID string) (int, error)
}

type StoredMessage struct {
    ID         int64     `json:"id"`
    SessionID  string    `json:"-"`
    Role       string    `json:"role"`
    Content    string    `json:"content"`
    TokensUsed int       `json:"tokens_used"`
    CreatedAt  time.Time `json:"created_at"`
}

Write-Through 模式

// internal/session/memory.go — AppendMessage 改造

func (m *MemoryManager) AppendMessage(ctx context.Context, sessionID string, msg models.Message) error {
    // 1. 写热存储(内存/Redis
    m.mu.Lock()
    entry, ok := m.sessions[sessionID]
    if !ok {
        m.mu.Unlock()
        return ErrSessionNotFound
    }
    entry.history = append(entry.history, msg)
    entry.lastActive = time.Now()
    if msg.Role == "user" && entry.session.Title == "新对话" {
        entry.session.Title = generateTitle(msg.Content)
    }
    if len(entry.history) > m.maxHistory {
        entry.history = entry.history[len(entry.history)-m.maxHistory:]
    }
    m.mu.Unlock()

    // 2. 写冷存储PostgreSQL异步不阻塞
    if m.msgRepo != nil {
        go func() {
            if err := m.msgRepo.SaveMessage(context.Background(), sessionID, msg, 0); err != nil {
                logger.Log.Warnw("persist message failed", "session", sessionID, "error", err)
            }
        }()
    }

    return nil
}

Phase 9旧端点废弃 + 集成收尾

目标:废弃旧的 /api/sessions 端点,完成全链路集成。

# 任务 文件 说明
9.1 废弃旧 session 路由 internal/api/session.go 保留代码但标记 deprecated或直接删除
9.2 main.go 完整组装 cmd/server/main.go 按 storage.driver 选择注入 MemoryRepo 或 PgRepo
9.3 .env.example 更新 backend/.env.example 新增 CAMTALK_AUTH_JWT_SECRETCAMTALK_STORAGE_*
9.4 config.yaml 更新 backend/config.yaml 新增 auth 配置块
9.5 docker-compose 添加 PG docker-compose.yml PostgreSQL 15 服务 + 环境变量
9.6 go mod tidy backend/ 清理依赖
9.7 端到端手动测试 注册 → 登录 → 创建对话 → 发送消息 → 登出 → 重新登录 → 查看对话列表 → 选择历史对话继续

main.go 依赖注入全貌

func main() {
    cfg, _ := config.Load()
    logger.Init(cfg.Log.Level, cfg.Log.Format)

    // --- 存储层 ---
    var (
        userRepo    store.UserRepository
        msgRepo     store.MessageRepository
        sessionMgr  session.Manager
    )

    if cfg.Storage.Driver == "postgres" {
        pool, _ := store.NewPostgresPool(ctx, cfg.Storage.DSN)
        defer pool.Close()
        userRepo = store.NewPgUserRepository(pool)
        msgRepo = store.NewPgMessageRepository(pool)
        sessionMgr = session.NewMemoryManager(..., msgRepo) // 注入 msgRepo
    } else {
        userRepo = store.NewMemUserRepository()
        sessionMgr = session.NewMemoryManager(...) // 无 msgRepo纯内存
    }

    // --- 认证 ---
    tokenMgr := auth.NewTokenManager(cfg.Auth.JWTSecret,
        time.Duration(cfg.Auth.AccessTTL)*time.Minute,
        time.Duration(cfg.Auth.RefreshTTL)*time.Minute)
    authService := auth.NewAuthService(tokenMgr, userRepo)

    // --- AI 服务(不变)---
    sttService := ...
    llmService := ...
    ttsService := ...
    orch := orchestrator.New(sttService, llmService, ttsService, sessionMgr, cfg)

    // --- 路由 ---
    r := gin.New()
    apiGroup := r.Group("/api")
    apiGroup.GET("/health", healthHandler(sessionMgr, cfg))

    authHandler := api.NewAuthHandler(authService)
    authHandler.RegisterRoutes(apiGroup)

    convHandler := api.NewConversationHandler(sessionMgr, tokenMgr)
    convHandler.RegisterRoutes(apiGroup)

    r.GET("/ws", ws.ServeWS(sessionMgr, orch, cfg, tokenMgr))

    // ... 启动
}

关键文件清单

backend/
  cmd/server/main.go                        ← Phase 1.6, 4.4, 7.5, 9.2
  migrations/
    001_users.up.sql                        ← Phase 1.4(新建)
    001_users.down.sql                      ← Phase 1.5(新建)
  internal/
    config/config.go                        ← Phase 1.1, 1.2(修改)
    errors/codes.go                         ← Phase 4.2(修改,新增错误码)
    models/models.go                        ← Phase 5.1(修改)
    store/
      db.go                                 ← Phase 1.3(新建)
      user.go                               ← Phase 2.2(新建)
      user_pg.go                            ← Phase 2.3(新建)
      user_mem.go                           ← Phase 2.4(新建)
      user_pg_test.go                       ← Phase 2.5(新建)
      message.go                            ← Phase 8.1(新建)
      message_pg.go                         ← Phase 8.2(新建)
    auth/
      jwt.go                                ← Phase 3.1(新建)
      password.go                           ← Phase 3.2(新建)
      middleware.go                         ← Phase 3.3(新建)
      service.go                            ← Phase 3.4, 3.5(新建)
      jwt_test.go                           ← Phase 3.6(新建)
      service_test.go                       ← Phase 3.7(新建)
    session/
      manager.go                            ← Phase 5.2(修改)
      memory.go                             ← Phase 5.3, 7.4, 8.3, 8.4(修改)
      redis.go                              ← Phase 5.4(修改)
      memory_test.go                        ← Phase 5.5(修改)
    api/
      auth.go                               ← Phase 4.1, 4.3(新建)
      auth_test.go                          ← Phase 4.5(新建)
      conversation.go                       ← Phase 6.1, 6.2, 6.3(新建)
      conversation_test.go                  ← Phase 6.5(新建)
      session.go                            ← Phase 9.1(废弃/删除)
    ws/
      handler.go                            ← Phase 7.1, 7.2, 7.3(修改)
      handler_test.go                       ← Phase 7.7(修改)

执行顺序与依赖关系

Phase 1 (配置 + DB 连接)
  ↓
Phase 2 (User 模型 + Repository)  ← 依赖 Phase 1
  ↓
Phase 3 (JWT + AuthService)        ← 依赖 Phase 2
  ↓
Phase 4 (Auth REST API)            ← 依赖 Phase 3
  ↓
Phase 5 (Session Manager 改造)     ← 依赖 Phase 1模型扩展可与 Phase 2-4 并行
  ↓
Phase 6 (Conversation REST API)    ← 依赖 Phase 4 + 5
  ↓
Phase 7 (WS 认证集成)              ← 依赖 Phase 3 + 5
  ↓
Phase 8 (消息持久化)               ← 依赖 Phase 1 + 5
  ↓
Phase 9 (废弃旧端点 + 集成收尾)    ← 依赖全部

可并行的路径

  • Phase 2-4用户认证链路和 Phase 5Session 改造)可并行开发
  • Phase 6对话 API和 Phase 7WS 认证)可并行开发

验证方案

层级 方法 覆盖范围
单元测试 go test ./internal/auth/... ./internal/store/... JWT 生成/校验、密码 hash、Repository CRUD
API 测试 httptest + go test ./internal/api/... 注册/登录/刷新/登出、对话 CRUD、权限校验
集成测试 启动 Gin test server + WS client WS 认证、conversation_id 恢复、消息持久化
端到端 手动测试 注册 → 登录 → 对话 → 登出 → 重登 → 历史列表 → 继续对话
静态检查 go vet ./... + go test ./... 全量通过