diff --git a/docs/superpowers/specs/2026-09-08-bookmarks-design.md b/docs/superpowers/specs/2026-09-08-bookmarks-design.md new file mode 100644 index 0000000..a08a667 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-bookmarks-design.md @@ -0,0 +1,77 @@ +# 书签系统(备注 + 跳转)设计 / 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` 追加(新表,无迁移痛): + +```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 + +全局书签页、书详情页书签区、去重、标签/分组、跨用户共享、书签导出。有真实需求再做。