- 新增 AuthConfig 结构体(JWTSecret, AccessTTL, RefreshTTL) - 在 Config 中添加 Auth 字段 - 设置默认值:access_ttl=15分钟,refresh_ttl=10080分钟(7天) - JWTSecret 必须通过环境变量 CAMTALK_AUTH_JWT_SECRET 设置
28 KiB
28 KiB
CamTalk 后端用户模块构建计划
Context
后端 AI 管道(STT → LLM → TTS)已完成,现在需要实现用户系统和对话持久化。目标:用户注册登录后,可在对话列表中选择历史对话继续交谈。
设计文档:docs/11-持久化与用户系统设计.md(最高依据)
技术选型:docs/04-技术选型.md 第四章
现有后端计划:docs/PLAN_BACKEND.md(AI 管道部分已完成)
当前后端状态
| 模块 | 状态 |
|---|---|
| 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 |
新增 AuthConfig(JWTSecret, AccessTTL, RefreshTTL),StorageConfig 已有 Driver/DSN 字段 |
| 1.2 | 添加配置默认值 | internal/config/config.go |
auth.access_ttl 默认 15,auth.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 3:JWT + 认证服务
目标:实现 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 5:Session 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_id、title 字段;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 7:WebSocket 认证集成
目标: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.SaveMessage(write-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_SECRET、CAMTALK_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 5(Session 改造)可并行开发
- Phase 6(对话 API)和 Phase 7(WS 认证)可并行开发
验证方案
| 层级 | 方法 | 覆盖范围 |
|---|---|---|
| 单元测试 | 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 ./... |
全量通过 |