package store import ( "context" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type Store struct{ P *pgxpool.Pool } func New(p *pgxpool.Pool) *Store { return &Store{P: p} } // ---------- types ---------- type User struct { ID int64 Username string PasswordHash string Role string CreatedAt time.Time } type Library struct { ID int64 Name string RootPath string CreatedAt time.Time } type Book struct { ID, LibraryID int64 Path string Title string Format string FileSize int64 ModTS int64 PageCount int // 0 表示未知(pdf/epub/txt) State string ErrMsg string AddedAt time.Time } type BookMeta struct { ID int64 Size int64 ModTS int64 Format string } type BookView struct { Book LibraryName string Percent float64 } type Progress struct { LibraryID int64 LibraryName string BookPath string Title string // 书已删时为空 Locator []byte Percent float64 UpdatedAt time.Time } // ---------- users ---------- const userCols = "id, username, password_hash, role, created_at" func (s *Store) CountUsers(ctx context.Context) (int, error) { var n int err := s.P.QueryRow(ctx, "SELECT count(*) FROM users").Scan(&n) return n, err } func (s *Store) CreateUser(ctx context.Context, username, hash, role string) (int64, error) { var id int64 err := s.P.QueryRow(ctx, "INSERT INTO users (username, password_hash, role) VALUES ($1,$2,$3) RETURNING id", username, hash, role).Scan(&id) return id, err } func (s *Store) GetUserByName(ctx context.Context, username string) (User, error) { return scanUser(s.P.QueryRow(ctx, "SELECT "+userCols+" FROM users WHERE username=$1", username)) } func (s *Store) GetUserByID(ctx context.Context, id int64) (User, error) { return scanUser(s.P.QueryRow(ctx, "SELECT "+userCols+" FROM users WHERE id=$1", id)) } func (s *Store) ListUsers(ctx context.Context) ([]User, error) { rows, err := s.P.Query(ctx, "SELECT "+userCols+" FROM users ORDER BY id") if err != nil { return nil, err } defer rows.Close() var out []User for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &u.CreatedAt); err != nil { return nil, err } out = append(out, u) } return out, rows.Err() } func (s *Store) DeleteUser(ctx context.Context, id int64) error { _, err := s.P.Exec(ctx, "DELETE FROM users WHERE id=$1", id) return err } func scanUser(row pgx.Row) (User, error) { var u User err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &u.CreatedAt) return u, err } // CountAdmins 供 Task 5 的"最后一个 admin 不可删"保护 func (s *Store) CountAdmins(ctx context.Context) (int, error) { var n int err := s.P.QueryRow(ctx, "SELECT count(*) FROM users WHERE role='admin'").Scan(&n) return n, err } // ---------- libraries ---------- func (s *Store) CreateLibrary(ctx context.Context, name, root string) (int64, error) { var id int64 err := s.P.QueryRow(ctx, "INSERT INTO libraries (name, root_path) VALUES ($1,$2) RETURNING id", name, root).Scan(&id) return id, err } func (s *Store) ListLibraries(ctx context.Context) ([]Library, error) { rows, err := s.P.Query(ctx, "SELECT id, name, root_path, created_at FROM libraries ORDER BY id") if err != nil { return nil, err } defer rows.Close() var out []Library for rows.Next() { var l Library if err := rows.Scan(&l.ID, &l.Name, &l.RootPath, &l.CreatedAt); err != nil { return nil, err } out = append(out, l) } return out, rows.Err() } func (s *Store) GetLibrary(ctx context.Context, id int64) (Library, error) { var l Library err := s.P.QueryRow(ctx, "SELECT id, name, root_path, created_at FROM libraries WHERE id=$1", id). Scan(&l.ID, &l.Name, &l.RootPath, &l.CreatedAt) return l, err } // ---------- books ---------- const bookCols = "id, library_id, path, title, format, file_size, mod_ts, page_count, state, error_msg, added_at" func (s *Store) InsertBook(ctx context.Context, libID int64, path, title, format string, size, modTS int64, pageCount int) (int64, error) { var id int64 err := s.P.QueryRow(ctx, `INSERT INTO books (library_id, path, title, format, file_size, mod_ts, page_count) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`, libID, path, title, format, size, modTS, pageCount).Scan(&id) return id, err } func (s *Store) GetBook(ctx context.Context, id int64) (Book, error) { var b Book err := s.P.QueryRow(ctx, "SELECT "+bookCols+" FROM books WHERE id=$1", id).Scan( &b.ID, &b.LibraryID, &b.Path, &b.Title, &b.Format, &b.FileSize, &b.ModTS, &b.PageCount, &b.State, &b.ErrMsg, &b.AddedAt) return b, err } func (s *Store) ListBookMeta(ctx context.Context, libID int64) (map[string]BookMeta, error) { rows, err := s.P.Query(ctx, "SELECT id, path, file_size, mod_ts, format FROM books WHERE library_id=$1", libID) if err != nil { return nil, err } defer rows.Close() out := map[string]BookMeta{} for rows.Next() { var m BookMeta var path string if err := rows.Scan(&m.ID, &path, &m.Size, &m.ModTS, &m.Format); err != nil { return nil, err } out[path] = m } return out, rows.Err() } func (s *Store) UpdateBookFile(ctx context.Context, id, size, modTS int64, pageCount int) error { _, err := s.P.Exec(ctx, `UPDATE books SET file_size=$2, mod_ts=$3, page_count=$4, state='ready', error_msg='' WHERE id=$1`, id, size, modTS, pageCount) return err } func (s *Store) DeleteBookByPath(ctx context.Context, libID int64, path string) error { _, err := s.P.Exec(ctx, "DELETE FROM books WHERE library_id=$1 AND path=$2", libID, path) return err } func (s *Store) DeleteBook(ctx context.Context, id int64) error { _, err := s.P.Exec(ctx, "DELETE FROM books WHERE id=$1", id) return err } func (s *Store) SetBookState(ctx context.Context, id int64, state, msg string) error { _, err := s.P.Exec(ctx, "UPDATE books SET state=$2, error_msg=$3 WHERE id=$1", id, state, msg) return err } func (s *Store) ListBookIDs(ctx context.Context) ([]int64, error) { rows, err := s.P.Query(ctx, "SELECT id FROM books") if err != nil { return nil, err } defer rows.Close() var out []int64 for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { return nil, err } out = append(out, id) } return out, rows.Err() } func (s *Store) ListBooks(ctx context.Context, libID int64, q, prefix string, userID int64) ([]BookView, error) { rows, err := s.P.Query(ctx, `SELECT b.id, b.library_id, b.path, b.title, b.format, b.file_size, b.mod_ts, b.page_count, b.state, b.error_msg, b.added_at, l.name, COALESCE(p.percent, 0) FROM books b JOIN libraries l ON l.id = b.library_id LEFT JOIN reading_progress p ON p.user_id = $4 AND p.library_id = b.library_id AND p.book_path = b.path WHERE ($1 = 0 OR b.library_id = $1) AND ($2 = '' OR lower(b.title) LIKE '%' || lower($2) || '%') AND ($3 = '' OR b.path LIKE $3 || '%') ORDER BY l.name, b.path`, libID, q, prefix, userID) if err != nil { return nil, err } defer rows.Close() var out []BookView for rows.Next() { var v BookView err := rows.Scan(&v.ID, &v.LibraryID, &v.Path, &v.Title, &v.Format, &v.FileSize, &v.ModTS, &v.PageCount, &v.State, &v.ErrMsg, &v.AddedAt, &v.LibraryName, &v.Percent) if err != nil { return nil, err } out = append(out, v) } return out, rows.Err() } // ---------- progress ---------- func (s *Store) UpsertProgress(ctx context.Context, userID, libID int64, bookPath string, locator []byte, percent float64) error { _, err := s.P.Exec(ctx, `INSERT INTO reading_progress (user_id, library_id, book_path, locator, percent, updated_at) VALUES ($1,$2,$3,$4,$5,now()) ON CONFLICT (user_id, library_id, book_path) DO UPDATE SET locator=$4, percent=$5, updated_at=now()`, userID, libID, bookPath, locator, percent) return err } func (s *Store) ListProgress(ctx context.Context, userID int64) ([]Progress, error) { rows, err := s.P.Query(ctx, `SELECT p.library_id, l.name, p.book_path, COALESCE(b.title, ''), p.locator, p.percent, p.updated_at FROM reading_progress p JOIN libraries l ON l.id = p.library_id LEFT JOIN books b ON b.library_id = p.library_id AND b.path = p.book_path WHERE p.user_id = $1 ORDER BY p.updated_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var out []Progress for rows.Next() { var pr Progress if err := rows.Scan(&pr.LibraryID, &pr.LibraryName, &pr.BookPath, &pr.Title, &pr.Locator, &pr.Percent, &pr.UpdatedAt); err != nil { return nil, err } out = append(out, pr) } return out, rows.Err() } func (s *Store) GetProgress(ctx context.Context, userID, libID int64, bookPath string) (Progress, error) { var pr Progress err := s.P.QueryRow(ctx, `SELECT library_id, book_path, locator, percent, updated_at FROM reading_progress WHERE user_id=$1 AND library_id=$2 AND book_path=$3`, userID, libID, bookPath). Scan(&pr.LibraryID, &pr.BookPath, &pr.Locator, &pr.Percent, &pr.UpdatedAt) return pr, err }