feat(bookmarks): api+store — per-user CRUD, owner-scoped 404, percent-ordered list

This commit is contained in:
2026-09-08 01:03:44 +08:00
parent e117ad52d3
commit 0fe3bc3057
5 changed files with 282 additions and 0 deletions
+8
View File
@@ -19,3 +19,11 @@ CREATE TABLE IF NOT EXISTS reading_progress (
locator JSONB NOT NULL DEFAULT '{}', percent DOUBLE PRECISION NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, library_id, book_path));
CREATE TABLE IF NOT EXISTS bookmarks (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
library_id BIGINT NOT NULL, book_path TEXT NOT NULL,
locator JSONB NOT NULL, percent DOUBLE PRECISION NOT NULL DEFAULT 0,
note TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now());
CREATE INDEX IF NOT EXISTS bookmarks_user_book_idx ON bookmarks (user_id, library_id, book_path);
+51
View File
@@ -314,6 +314,57 @@ func (s *Store) ListProgress(ctx context.Context, userID int64) ([]Progress, err
return out, rows.Err()
}
// ---------- bookmarks ----------
type Bookmark struct {
ID int64
LibraryID int64
BookPath string
Locator []byte
Percent float64
Note string
CreatedAt time.Time
}
func (s *Store) InsertBookmark(ctx context.Context, userID, libID int64, bookPath string, locator []byte, percent float64, note string) (int64, error) {
var id int64
err := s.P.QueryRow(ctx,
`INSERT INTO bookmarks (user_id, library_id, book_path, locator, percent, note)
VALUES ($1,$2,$3,$4,$5,$6) RETURNING id`,
userID, libID, bookPath, locator, percent, note).Scan(&id)
return id, err
}
func (s *Store) ListBookmarks(ctx context.Context, userID, libID int64, bookPath string) ([]Bookmark, error) {
rows, err := s.P.Query(ctx,
`SELECT id, library_id, book_path, locator, percent, note, created_at
FROM bookmarks WHERE user_id=$1 AND library_id=$2 AND book_path=$3
ORDER BY percent ASC, id ASC`, userID, libID, bookPath)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Bookmark
for rows.Next() {
var b Bookmark
if err := rows.Scan(&b.ID, &b.LibraryID, &b.BookPath, &b.Locator, &b.Percent, &b.Note, &b.CreatedAt); err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
func (s *Store) UpdateBookmarkNote(ctx context.Context, userID, id int64, note string) (bool, error) {
res, err := s.P.Exec(ctx, `UPDATE bookmarks SET note=$3 WHERE id=$1 AND user_id=$2`, id, userID, note)
return res.RowsAffected() > 0, err
}
func (s *Store) DeleteBookmark(ctx context.Context, userID, id int64) (bool, error) {
res, err := s.P.Exec(ctx, `DELETE FROM bookmarks WHERE id=$1 AND user_id=$2`, id, userID)
return res.RowsAffected() > 0, err
}
func (s *Store) GetProgress(ctx context.Context, userID, libID int64, bookPath string) (Progress, error) {
var pr Progress
err := s.P.QueryRow(ctx,