mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
95 lines
9.7 KiB
Markdown
95 lines
9.7 KiB
Markdown
# 阅间 Demo 设计与数据流
|
||
|
||
## 页面分工
|
||
|
||
统计并入阅读概览;导航保留阅读概览、我的书架、阅读目标、笔记与划线、发现好书。概览展示阅读统计和目标摘要,目标页管理多份具体阅读清单。
|
||
|
||
完整功能、业务规则、接口落点与状态见 [功能目标清单](feature-checklist.md)。该清单先于本轮实现建立,后续代码开发继续追加和更新。
|
||
|
||
该设计探索已归档,当前正式看板由根目录 Flask 服务提供。
|
||
|
||
## 数据流
|
||
|
||
`demo-data.js` 提供样例书籍、书单及初始目标。`app.js` 根据当前页面和筛选条件渲染内容;用户输入写入目标草稿,验证通过后保存到浏览器,再更新目标列表。现有 Flask 服务和数据采集代码不参与本轮 Demo。
|
||
|
||
书籍卡片打开详情;详情页可选目录、个人笔记、热门划线、点评、相似书。个人笔记与公开点评使用不同数据集合。热门划线中的想法继续打开单条详情。
|
||
|
||
书籍、书单详情均可加入已有目标,也可带着已选内容创建新目标。
|
||
|
||
## 阅读目标口径与例子
|
||
|
||
目标字段:`id`、`name`、`start`、`end`、`ids`、`sources`。`ids` 存放去重后的书籍 ID;`sources` 记录曾加入的书单名称,作为来源说明,不表示持续关联。
|
||
|
||
例:单独选择已读完的《活着》,再加入包含《活着》《人生海海》《悉达多》的书单。保存后总数为 3 本、已读完为 1 本,进度为 33%。从目标中移除《人生海海》后,进度为 1 / 2,即 50%。
|
||
|
||
完成状态取书籍当前已读状态,不受目标起止日期约束。同一本书可计入多个目标。同一目标内不重复计数。
|
||
|
||
状态判断先检查是否全部读完,再检查日期。例如目标时间为未来一周,但其中两本书都已读完,直接显示已完成;尚未全部完成时,按日期显示未开始、进行中、已到期。
|
||
|
||
书单采用加入时的书目,不自动跟随外部增删。移除目标内一本书,只影响这个目标,不改变书架或其他目标。
|
||
|
||
## 实现位置
|
||
|
||
| 职责 | 位置 |
|
||
|---|---|
|
||
| 去重与完成率、状态 | `app.js`:`uniqueIds`、`goalMetrics` |
|
||
| 新建、编辑、书单混选 | `app.js`:`openGoalEditor`、`renderPicker`、`saveGoal` |
|
||
| 加入已有目标 | `app.js`:`chooseTarget`、`appendGoal` |
|
||
| 本地持久化与异常恢复 | `app.js`:初始加载、`persistGoals` |
|
||
| 页面与详情导航 | `app.js`:`navigate`、`renderPage`、`handleAction` |
|
||
| 笔记导出 | `app.js`:`exportNotes` |
|
||
|
||
用户输入的目标名称和来源在 HTML 渲染前转义。日期和内容在保存前校验;原生对话框提供焦点管理和 Escape 关闭。
|
||
|
||
## 部署流程
|
||
|
||
GitHub Actions 的 `.github/workflows/deploy.yml` 只监听 `master` 分支。分支 push 后,Actions 使用仓库 Secrets 中的服务器地址、用户和 SSH 私钥登录服务器,进入 `/opt/weread-board/WeReadBoard` 后调用 `deploy.sh`。手动触发也固定部署 `master`,部署脚本会拒绝其他分支。
|
||
|
||
`deploy.sh` 固定拉取 `master`,收到其他部署分支参数时直接退出,再检查项目 `.venv`。如果 `.venv/bin/python` 不存在,脚本使用服务器上的 `python3` 或 `python` 创建虚拟环境;随后用 `.venv/bin/python -m pip install -r requirements.txt` 安装依赖,按文件名顺序执行 `migrations/*.sql`,最后重启 `weread.service`。
|
||
|
||
例:用户将已验收代码合并并推送到 `master`,服务器原来没有 `.venv`。部署脚本会在 `/opt/weread-board/WeReadBoard/.venv` 创建环境,安装 Flask 和 PyMySQL,确认阅读目标表存在,再让 systemd 重新用 `.venv/bin/python app.py` 启动服务。仅推送 `dev` 不会触发部署。
|
||
|
||
## 正式看板登录认证
|
||
|
||
`auth.py` 注册全站请求拦截和登录路由。`/` 与所有 `/api/*` 数据接口都需要 `session["auth_user"]`;静态资源、`/login`、`/auth/login` 和首次初始化路由是唯一公开入口。页面请求未认证时重定向到登录页,API 返回 401,避免前端遮罩之外仍可直接读取数据。旧 `/api/v2/*` 路由同样由 Flask 全局拦截。
|
||
|
||
账号表 `weread_auth` 固定使用 `id=1` 保存单管理员的用户名和 Werkzeug 密码哈希。数据库无账号时,只有提交服务端 `WEREAD_AUTH_SETUP_TOKEN` 才能插入首个账号;例如提交用户名 `reader`、一次性口令正确、密码 `a-long-private-password`,数据库保存的是哈希而不是密码。插入成功后创建永久 Flask 会话,并清空当前进程中的初始化口令。后续登录从 MySQL 取哈希校验;用户名或密码错误统一返回“账号或密码不正确”。
|
||
|
||
会话签名密钥从 `WEREAD_SESSION_SECRET` 读取且必须稳定配置;缺失时服务对动态页面和 API 以 503 失败关闭。永久会话时长是固定 5 天,不因每次请求续期。Cookie 为 HttpOnly、SameSite=Lax,并默认 Secure;仅本机 HTTP 预览可以关闭 Secure。生产站点须使用 HTTPS。前端 JSON 写请求从页面 meta 标签读取 CSRF token 并放入 `X-CSRF-Token` 请求头,表单请求使用隐藏字段。
|
||
|
||
应用未内置登录尝试限流;公网部署应在可信反向代理上为 `POST /auth/login` 设置频率限制,且不要直接信任未校验的 `X-Forwarded-For`。
|
||
|
||
## 示例与真实接口的边界
|
||
|
||
概览图表使用固定样例;目标日期状态使用浏览器当前本地日期。笔记导出包含当前选中书目的划线和个人想法全集,不受内容类型筛选影响,不包含书签内容及公开点评。
|
||
|
||
书架的有声内容完结不等于用户读完,所以未用其完结状态计算阅读目标。文章收藏是入口,不假装能直接列出内部文章。缺失资料显示暂无,不补造 ISBN 等字段。
|
||
|
||
正式接口接入单独推进,必须核对实时可用接口、鉴权、分页、错误、升级提示及字段口径;本轮 Demo 的完成状态不得用于宣称这些能力已真实接入。
|
||
|
||
## 正式看板的紧凑布局
|
||
|
||
正式看板在 `static/css/styles.css` 中统一调整页面密度。顶栏高度为桌面 58 px、手机 56 px;标题区采用更短的上下间距,隐藏页面标题前的英文装饰文字。标题、说明、筛选、内容依次展示,操作按钮在桌面与标题并排,手机上自然换行。
|
||
|
||
书架按可用宽度自动排列书籍,每列约 116 px 起步;手机固定两列。真实封面维持原比例,并受所在列宽度约束。例如,书架网格可用宽度为 900 px 时显示多列紧凑排布,手机宽度为 390 px 时显示 2 列,长书名仍使用已有省略规则。
|
||
|
||
目标介绍区在宽屏上把说明与统计并排,窄屏上改为上下排列;目标卡按可用宽度自动排列,每张卡约 280 px 起步,并缩小封面预览和上下间距。例如,一个包含 6 本书、已读完 2 本的月度目标仍展示原有完成数和进度,只减少卡片占位。页面渲染、筛选、目标计算和数据流保持由现有 JavaScript 实现,CSS 不改变业务口径。
|
||
|
||
## 正式看板的阅读目标持久化
|
||
|
||
正式看板使用 `static/js/app.js`、`static/js/weapi.js` 和 `reading_api.py`。本节覆盖正式页面;前文的 Demo 存储方式不适用于正式目标。
|
||
|
||
`reading_goals` 保存 `id`、`name`、`start_date`、`end_date`、来源书单名称 `sources` 和创建、修改时间。`reading_goal_books` 保存 `goal_id`、`book_id`,联合主键防止同一目标重复计书。目标 ID 沿用前端生成值,便于导入旧目标。来源名称保存为 JSON 数组,不表示持续关联书单。
|
||
|
||
目标成员只通过外键关联目标。删除目标时级联删除成员,书架不受影响;书籍移出书架时也不删除目标成员。迁移位于 `migrations/001_reading_goals.sql`,只需部署时执行,不在请求中自动建表。
|
||
|
||
页面加载时 GET 目标列表,新建时 POST,编辑及追加书籍时 PUT,删除时 DELETE。表单先校验名称、日期和书目,服务端再次校验。目标与成员在同一事务中提交,编辑时锁定目标父记录并整体替换成员,避免并发请求混入两份书目。多个浏览器同时编辑同一目标时,以最后成功保存的完整内容为准。
|
||
|
||
前端收到提交成功响应后更新内存并关闭表单。保存中阻止重复提交;目标列表未从数据库读取完成前,不打开新建或编辑窗口,避免初始读取失败状态覆盖刚保存的目标。新建重试保持原 ID。数据库错误转换为 503,回滚事务并让前端保留草稿。目标列表不使用进程缓存,刷新即读取数据库。
|
||
|
||
例:创建“秋季阅读”,日期为 `2026-09-01` 至 `2026-09-30`,输入书籍 `["A", "A", "B"]` 和来源 `["文学书单"]`。数据库生成 1 条目标、2 条成员。若 A 已读完而 B 未读完,页面显示 `1 / 2,50%`;B 暂时不在书架中时仍显示 `1 / 2,50%`,B 的资料占位显示“状态待同步”,可在编辑器移除。若用户明确移除 B,则显示 `1 / 1,100%`。
|
||
|
||
完成率和状态不写入目标表。当前已知已读状态用于计算,不要求在目标日期内读完;全部已读完优先显示“已完成”,否则按浏览器本地日期显示未开始、进行中或已到期,起止当天均属于进行中。未知状态不计为已完成,但仍计入分母。
|
||
|
||
旧数据仅在用户点击导入时发送给 `/api/reading/goals/import`。先校验整批,再在事务中逐项插入;相同 ID 已存在时保留数据库内容,批次内重复 ID 只处理第一条。例如库中已有 G1,导入 `[G1, G2, G2]` 返回 `{"imported":1}`,再次导入返回 `{"imported":0}`。任何写入失败均回滚整批;成功后保留原浏览器备份并记录导入标记。
|