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

78 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 书签系统(备注 + 跳转)设计 / 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
全局书签页、书详情页书签区、去重、标签/分组、跨用户共享、书签导出。有真实需求再做。