docs(bookmarks): design spec — per-user bookmarks table, REST CRUD with owner-scoped 404s, capture/seek reuse of progress locators, shared sidebar component
This commit is contained in:
@@ -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
|
||||
|
||||
全局书签页、书详情页书签区、去重、标签/分组、跨用户共享、书签导出。有真实需求再做。
|
||||
Reference in New Issue
Block a user