feat: 实现 sessions 持久化,支持会话恢复
This commit is contained in:
47
backend/internal/store/session.go
Normal file
47
backend/internal/store/session.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSessionNotFound 会话不存在。
|
||||
ErrSessionNotFound = errors.New("session not found")
|
||||
)
|
||||
|
||||
// SessionRepository 会话持久化接口。
|
||||
type SessionRepository interface {
|
||||
// Save 创建或更新会话(UPSERT)。
|
||||
Save(ctx context.Context, s SessionRecord) error
|
||||
|
||||
// FindByID 根据 ID 查询会话。
|
||||
FindByID(ctx context.Context, id string) (*SessionRecord, error)
|
||||
|
||||
// FindByUser 查询用户的会话列表(分页,按 updated_at 降序)。
|
||||
// 返回 (列表, 总数, error)。
|
||||
FindByUser(ctx context.Context, userID string, page, size int) ([]SessionRecord, int, error)
|
||||
|
||||
// UpdateTitle 更新会话标题。
|
||||
UpdateTitle(ctx context.Context, id string, title string) error
|
||||
|
||||
// UpdateConfig 更新会话配置。
|
||||
UpdateConfig(ctx context.Context, id string, configJSON []byte) error
|
||||
|
||||
// Touch 刷新 updated_at。
|
||||
Touch(ctx context.Context, id string) error
|
||||
|
||||
// Delete 删除会话。
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// SessionRecord 持久化会话模型(store 层)。
|
||||
type SessionRecord struct {
|
||||
ID string
|
||||
UserID string
|
||||
Title string
|
||||
Config []byte // JSON 编码的 SessionConfig
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
146
backend/internal/store/session_pg.go
Normal file
146
backend/internal/store/session_pg.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// PgSessionRepository 基于 PostgreSQL 的 SessionRepository 实现。
|
||||
type PgSessionRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewPgSessionRepository 创建 PgSessionRepository。
|
||||
func NewPgSessionRepository(pool *pgxpool.Pool) *PgSessionRepository {
|
||||
return &PgSessionRepository{pool: pool}
|
||||
}
|
||||
|
||||
func (r *PgSessionRepository) Save(ctx context.Context, s SessionRecord) error {
|
||||
_, err := r.pool.Exec(ctx,
|
||||
`INSERT INTO sessions (id, user_id, title, config, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
config = EXCLUDED.config,
|
||||
updated_at = EXCLUDED.updated_at`,
|
||||
s.ID, s.UserID, s.Title, s.Config, s.CreatedAt, s.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *PgSessionRepository) FindByID(ctx context.Context, id string) (*SessionRecord, error) {
|
||||
var s SessionRecord
|
||||
err := r.pool.QueryRow(ctx,
|
||||
`SELECT id, user_id, title, config, created_at, updated_at
|
||||
FROM sessions WHERE id = $1`, id,
|
||||
).Scan(&s.ID, &s.UserID, &s.Title, &s.Config, &s.CreatedAt, &s.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrSessionNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *PgSessionRepository) FindByUser(ctx context.Context, userID string, page, size int) ([]SessionRecord, int, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
offset := (page - 1) * size
|
||||
|
||||
// 查询总数
|
||||
var total int
|
||||
if err := r.pool.QueryRow(ctx,
|
||||
`SELECT COUNT(*) FROM sessions WHERE user_id = $1`, userID,
|
||||
).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 查询列表
|
||||
rows, err := r.pool.Query(ctx,
|
||||
`SELECT id, user_id, title, config, created_at, updated_at
|
||||
FROM sessions
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $2 OFFSET $3`,
|
||||
userID, size, offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var list []SessionRecord
|
||||
for rows.Next() {
|
||||
var s SessionRecord
|
||||
if err := rows.Scan(&s.ID, &s.UserID, &s.Title, &s.Config, &s.CreatedAt, &s.UpdatedAt); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
list = append(list, s)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (r *PgSessionRepository) UpdateTitle(ctx context.Context, id string, title string) error {
|
||||
tag, err := r.pool.Exec(ctx,
|
||||
`UPDATE sessions SET title = $2, updated_at = NOW() WHERE id = $1`,
|
||||
id, title,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PgSessionRepository) UpdateConfig(ctx context.Context, id string, configJSON []byte) error {
|
||||
tag, err := r.pool.Exec(ctx,
|
||||
`UPDATE sessions SET config = $2, updated_at = NOW() WHERE id = $1`,
|
||||
id, configJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PgSessionRepository) Touch(ctx context.Context, id string) error {
|
||||
tag, err := r.pool.Exec(ctx,
|
||||
`UPDATE sessions SET updated_at = NOW() WHERE id = $1`, id,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PgSessionRepository) Delete(ctx context.Context, id string) error {
|
||||
tag, err := r.pool.Exec(ctx,
|
||||
`DELETE FROM sessions WHERE id = $1`, id,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user