Files
book-comic-library/docs/superpowers/specs/2026-09-08-bookmarks-design.md

3.8 KiB
Raw Permalink Blame History

书签系统(备注 + 跳转)设计 / Bookmarks (notes + jump-to) design

日期 2026-09-08。分支 feat/bookmarks。状态:已获用户批准(会话内确认)。

目标 / Goal

阅读任意格式的书时可「加书签」,书签携带备注,可从书签列表一键跳转(seek)到原位。 Bookmarks with editable notes and one-click jump-to-position for all reader formats.

决策记录 / Decisions

  • 个人可见(与 progress 同款 user 隔离),不做共享。用户选定。
  • 阅读器内闭环(工具条按钮 + 侧栏),不做全局书签页。用户选定。
  • 全格式(cbz/txt/md/pdf/epub):locator 存取/恢复机制四类 reader 均已存在,书签复用。用户选定。
  • 否决方案:书签数组塞 progress JSONB(整包重写、并发互踩);纯 localStorage(不跨设备、与服务端进度先例分裂)。
  • 删书不级联清书签:与 reading_progress 既有行为一致(孤儿行 JOIN 不中即无害)。

数据模型 / Schema

backend/internal/db/schema.sql 追加(新表,无迁移痛):

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);

定位键与 progress 相同 (user_id, library_id, book_path);locator 各格式形状不变 (cbz/pdf {page},txt/md {ch,scrollFraction},epub {cfi})。

API(source of truth: cmd/webui/api/router.go)

方法/路径 语义 错误
GET /api/books/:id/bookmarks 本书书签,percent 升序 book 不存在 404
POST /api/books/:id/bookmarks {locator,percent,note?} 建,201 返回全字段 locator 空/percent∉[0,1]/note>500 字符 → 400
PATCH /api/bookmarks/:id {note} 仅改备注 非本人/不存在 → 404
DELETE /api/bookmarks/:id 删,204 非本人/不存在 → 404
  • (library_id, book_path) 由服务端从 :id 解析,客户端不传路径。
  • 全部登录可见(admin 不特殊);「非本人」统一 404,不泄露存在性。
  • store 层新增 ListBookmarks/InsertBookmark/UpdateBookmarkNote/DeleteBookmark,均带 user_id 条件。

前端 / Frontend

  • useProgressSaver 暴露 capture(): {locator, percent} | null——每次 report() 记下最新值, 打开时以该书已有进度(initialLocator/percent)播种。书签 = 当前进度快照 + 备注。
  • components/Bookmarks.tsx 共享组件(目录侧栏同款交互/样式 rd-*):
    • 工具条「书签」按钮 → 弹出备注输入(可空,Enter 保存) → POST → 刷新列表。
    • 侧栏行:percent% + 备注(空则省略) + 时间;点行 = seek(locator) 并收起;✎ 行内改备注;✕ 删除。
  • 各 reader 提供 seek(locator):cbz jump(page) 已有;txt goChapter+scrollFraction 已有; epub 复用初始恢复路径(display(cfi));pdf 补一个滚动到页的函数(~5 行)。

错误处理 / Errors

  • 网络失败:toast 报错,列表不半更新(react-query invalidate 全书)。
  • 竞态(两端同时加):无幂等要求,各自成条。
  • percent 越界/locator 非法:400,前端表单先行禁用空提交。

测试 / Tests

  • 后端 handlers/bookmarks_test.go:CRUD 全链路、他人 404、note 超长 400、book 不存在 404、 percent 越界 400、列表排序。
  • 前端:npm run check 绿;真实浏览器点验(加/跳/改/删)。不为组件引入新测试框架。
  • 回归:go vet + go test -p 1 ./...。

不做 / Non-goals

全局书签页、书详情页书签区、去重、标签/分组、跨用户共享、书签导出。有真实需求再做。