MVP 增补:书籍音频与封面附件(音频不与文本同步) #21

Closed
opened 2026-09-10 22:00:14 +08:00 by ila · 7 comments
Owner

This was generated by AI during triage.

来源与目标

用户在当前会话提出:"可以导入音频和点击播放音频,音频和文本不需要同步;然后给书本配上封面"。随后确认先建工单,再按 #4 → #5 的顺序推进;本次指令为"建工单"。建单时间:2026-09-10T22:00:11+08:00。

为本人已有书籍附加可选音频和封面,阅读时独立播放音频,书库更容易识别书籍。本单是 F01–F12 之外明确新增的 MVP 附件能力,关联总览 #16;不把原型、建单或既有 #3 小样视为本功能已实现。

已确认首版范围

  • 每本书可选上传一个音频文件;在阅读页主动点击播放,支持暂停、拖动进度、调整速度。
  • 记住该用户该书上次播放位置,重新打开后可继续;不自动播放,不做文本或章节对齐。
  • 每本书可选上传一张 JPG、PNG 或 WebP 封面;未设置时显示默认封面。
  • 在书籍编辑入口上传、替换或移除附件,书库展示封面,阅读页显示音频播放器;无音频的书籍仍能正常阅读。
  • 音频、封面和播放进度归属当前书籍所有者,附件访问不能绕过 Go 后端权限;备份恢复包含附件及关联数据。

非目标

多音轨、分章节音轨、文本时间戳同步、字幕对齐、逐句跟读、音频转文字、TTS、自动搜索封面、在线音频地址抓取、公开分享和跨用户附件共享不在本单范围。

验收标准

  • 上传本人书籍的音频和封面后,书库封面及阅读页播放器可用;无附件有默认/空状态,失败有可理解的反馈,不破坏原书籍。
  • 音频播放/暂停、拖动到指定位置、倍速、结束和加载失败行为明确;重新打开可以恢复播放位置,替换/移除音频时旧进度按已确认规则重置,不误用到新文件。
  • 上传格式白名单、文件大小/图片像素/存储限制及错误反馈在实现前固定;校验实际文件内容,不仅依赖扩展名。首版优先验证 MP3,最终支持格式以桌面和手机浏览器实测及原型确认记录为准,不承诺任意格式。
  • 两账号测试覆盖书籍 ID、附件 ID、文件 URL 和 Range 请求:未登录/他人无法读取、替换或移除附件,也不能写入他人的播放进度;退出或账号禁用后访问继续受控。
  • 替换/上传失败保留旧附件;文件与数据库引用一致。移除附件、删除书籍的清理与重试规则明确,不误删其他书籍文件。书籍删除由 #10 接入同一清理契约。
  • 在隔离测试数据中验证迁移/回退和备份恢复,恢复后附件可访问、归属与播放进度正确;#15 的整站恢复验收纳入本单附件。
  • 桌面与手机验证播放器和书库封面,不妨碍阅读/查词/短语选择;不出现音频与文本同步承诺。

依赖、顺序与状态

类型:功能增补。状态:待验收(2026-09-15,提交 ab23ff6,PR #36;契约见评论 8116,实施证据见评论 8124)。

前置:#5 基础导入与书籍阅读完成并验收(已完成)。

#10 是书籍编辑/删除的协同任务,不要求先完成全部 #10 才能设计本单;实施前核对其状态,复用已有编辑入口或提供最小附件编辑入口,避免重复建设。#15 负责最后整站备份恢复验收。

范围变更审议中(2026-09-15,未确认,不影响上述已交付范围):用户提出多篇网络文章合并成一本书多章、音频与新增插图改为章级的场景;第二次范围变更草案见评论 8129,等待用户确认后再生效并转回"进行中"。在用户明确确认 8129 之前,本工单的有效范围仍是上面"已确认首版范围"(书级音频+封面),与已交付代码一致。

设计证据与实现前检查

复用已验收 Quant-UX v1 的学习端风格;新增书籍附件编辑、书库封面与阅读播放器的桌面/手机关键状态,已按评论 8116 的标注线框(S1–S4)确认并实施。

技术设计见评论 8116:附件元数据、私人文件存储与授权传输(含 Range)、播放进度保存频率、文件替换一致性与清理恢复。

估算、风险与回退

初步规划 4~6 人日,含原型调整、前后端、附件权限、测试与文档;是 Agent 规划参考,不代表实际工时或日期承诺。主要风险是音频体积、浏览器格式兼容、鉴权媒体请求和附件清理一致性。

新增元数据先在隔离测试环境迁移/回退,保留现有书籍与原文;附件写入失败可重试,删除操作须遵循授权与清理契约,禁止以测试名义删除用户真实文件。

文档与证据

实施证据见评论 8124:Wiki 页面 revision、测试结果、未验证内容与回退方式。#15 部署/恢复文档已含附件存储范围。

Gitea MCP 指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅进入进程。项目使用现有正文状态流转,不新增其他技能的标签体系或本地工单目录。

> *This was generated by AI during triage.* ## 来源与目标 用户在当前会话提出:"可以导入音频和点击播放音频,音频和文本不需要同步;然后给书本配上封面"。随后确认先建工单,再按 #4 → #5 的顺序推进;本次指令为"建工单"。建单时间:2026-09-10T22:00:11+08:00。 为本人已有书籍附加可选音频和封面,阅读时独立播放音频,书库更容易识别书籍。本单是 F01–F12 之外明确新增的 MVP 附件能力,关联总览 #16;不把原型、建单或既有 #3 小样视为本功能已实现。 ## 已确认首版范围 - 每本书可选上传一个音频文件;在阅读页主动点击播放,支持暂停、拖动进度、调整速度。 - 记住该用户该书上次播放位置,重新打开后可继续;不自动播放,不做文本或章节对齐。 - 每本书可选上传一张 JPG、PNG 或 WebP 封面;未设置时显示默认封面。 - 在书籍编辑入口上传、替换或移除附件,书库展示封面,阅读页显示音频播放器;无音频的书籍仍能正常阅读。 - 音频、封面和播放进度归属当前书籍所有者,附件访问不能绕过 Go 后端权限;备份恢复包含附件及关联数据。 ## 非目标 多音轨、分章节音轨、文本时间戳同步、字幕对齐、逐句跟读、音频转文字、TTS、自动搜索封面、在线音频地址抓取、公开分享和跨用户附件共享不在本单范围。 ## 验收标准 - [ ] 上传本人书籍的音频和封面后,书库封面及阅读页播放器可用;无附件有默认/空状态,失败有可理解的反馈,不破坏原书籍。 - [ ] 音频播放/暂停、拖动到指定位置、倍速、结束和加载失败行为明确;重新打开可以恢复播放位置,替换/移除音频时旧进度按已确认规则重置,不误用到新文件。 - [ ] 上传格式白名单、文件大小/图片像素/存储限制及错误反馈在实现前固定;校验实际文件内容,不仅依赖扩展名。首版优先验证 MP3,最终支持格式以桌面和手机浏览器实测及原型确认记录为准,不承诺任意格式。 - [ ] 两账号测试覆盖书籍 ID、附件 ID、文件 URL 和 Range 请求:未登录/他人无法读取、替换或移除附件,也不能写入他人的播放进度;退出或账号禁用后访问继续受控。 - [ ] 替换/上传失败保留旧附件;文件与数据库引用一致。移除附件、删除书籍的清理与重试规则明确,不误删其他书籍文件。书籍删除由 #10 接入同一清理契约。 - [ ] 在隔离测试数据中验证迁移/回退和备份恢复,恢复后附件可访问、归属与播放进度正确;#15 的整站恢复验收纳入本单附件。 - [ ] 桌面与手机验证播放器和书库封面,不妨碍阅读/查词/短语选择;不出现音频与文本同步承诺。 ## 依赖、顺序与状态 类型:功能增补。状态:待验收(2026-09-15,提交 ab23ff6,PR #36;契约见评论 8116,实施证据见评论 8124)。 前置:#5 基础导入与书籍阅读完成并验收(已完成)。 #10 是书籍编辑/删除的协同任务,不要求先完成全部 #10 才能设计本单;实施前核对其状态,复用已有编辑入口或提供最小附件编辑入口,避免重复建设。#15 负责最后整站备份恢复验收。 **范围变更审议中(2026-09-15,未确认,不影响上述已交付范围)**:用户提出多篇网络文章合并成一本书多章、音频与新增插图改为章级的场景;第二次范围变更草案见评论 [8129](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8129),等待用户确认后再生效并转回"进行中"。在用户明确确认 8129 之前,本工单的有效范围仍是上面"已确认首版范围"(书级音频+封面),与已交付代码一致。 ## 设计证据与实现前检查 复用已验收 Quant-UX v1 的学习端风格;新增书籍附件编辑、书库封面与阅读播放器的桌面/手机关键状态,已按评论 8116 的标注线框(S1–S4)确认并实施。 技术设计见评论 8116:附件元数据、私人文件存储与授权传输(含 Range)、播放进度保存频率、文件替换一致性与清理恢复。 ## 估算、风险与回退 初步规划 4~6 人日,含原型调整、前后端、附件权限、测试与文档;是 Agent 规划参考,不代表实际工时或日期承诺。主要风险是音频体积、浏览器格式兼容、鉴权媒体请求和附件清理一致性。 新增元数据先在隔离测试环境迁移/回退,保留现有书籍与原文;附件写入失败可重试,删除操作须遵循授权与清理契约,禁止以测试名义删除用户真实文件。 ## 文档与证据 实施证据见评论 8124:Wiki 页面 revision、测试结果、未验证内容与回退方式。#15 部署/恢复文档已含附件存储范围。 Gitea MCP 指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅进入进程。项目使用现有正文状态流转,不新增其他技能的标签体系或本地工单目录。
Author
Owner

#21 设计证据(形式 b:标注线框 + 状态说明)与契约草案(2026-09-15)

用户 2026-09-15 确认「做 #21,按建议的默认 + 用 (b) 设计证据」。本评论先交付可审阅的设计证据与实施契约;按 #21 正文的门禁,新增交互需用户确认后编码,因此工单在本评论后置为「待确认」,用户确认即开始实施。

一、设计证据(标注线框)

复用现有规范与布局,不新增配色、不新增页面级导航。

S1 书库卡片(LibraryView,桌面/手机同构)

┌─────────────────────────┐
│ ┌─────────────────────┐ │  ← 封面 16:9,未设置=默认封面(淡色底 + 书名首字)
│ │       封面图         │ │     上传后立即生效,无需刷新
│ └─────────────────────┘ │
│ Fictional reader        │  ← 现有 .book-title
│ 已就绪 · 3 章            │  ← 现有状态摘要
└─────────────────────────┘

手机:单列、封面随宽度自适应,不产生横向滚动。

S2 书籍页「音频与封面」区块(BookView,位于现有 .page-actions 下方;不用对话框)

音频与封面
┌────────────────────────────────────────────────────────┐
│ 封面   [预览 160×90]   已设置 · JPG · 1.2 MB            │
│                         [替换封面] [移除]               │
│ 音频   MP3 · 3.4 MB    上次播放位置 12:30               │
│                         [上传音频] [替换] [移除]        │
└────────────────────────────────────────────────────────┘
  • 未设置时显示「未设置」+ 上传入口;不显示空的预览框。
  • 进行中:按钮 loading + 「正在上传…」;失败:区块内可读错误(格式/大小/网络),保留原附件。
  • 移除:二次确认(复用删除章节的确认样式)。
  • 手机:上下堆叠,按钮全宽。

S3 阅读页播放器条(ReaderView;仅当该书有音频时出现)

┌──────────────────────────────────────────────────────────┐
│ ▶/⏸   0:12 ──────●──────────── 12:30   1.0×   ↻ 重试     │
└──────────────────────────────────────────────────────────┘
   章节标题
   正文…
  • 位置:章节标题下方、正文上方;position: sticky; top: 0(手机同样 sticky)。
  • 控件:播放/暂停、进度(可拖动,原生 range,方向键可用)、倍速 0.75/1.0/1.25/1.5。
  • 无音频时不显示任何占位;加载失败时条内显示「音频加载失败 · 重试」。
  • 不自动播放;不与正文做任何同步或高亮。

S4 状态与持久化规则

状态 行为
打开书 只恢复进度显示,不自动播放
播放中 每 5 秒、暂停时、离开页面时上报位置(同一账号同一本书)
替换音频 旧进度重置为 0,避免套用到新文件
移除音频 删除进度记录,播放器条消失
其他账号 看不到附件播放位置,也不能读取附件(404)

二、实施契约

D1 数据(schema v8,新表,可重放 DDL)

  • lexgo_book_attachments:book_id + kind('audio'|'cover') 唯一,owner_id、mime、byte_size、sha256、bytes MEDIUMBLOB、时间戳;外键级联到书籍与账号。一本书最多一段音频、一张封面。
  • lexgo_playback_positions:主键 (owner_id, book_id),position_seconds、updated_at;外键级联到账号与书籍。
  • 沿用「只加新表、不用 ALTER TABLE」的既定做法,迁移与回退测试照 #13。

D2 接口

接口 说明
POST /api/v1/books/:id/cover、POST /api/v1/books/:id/audio multipart 单文件;成功返回书籍附件元数据;替换即覆盖
DELETE /api/v1/books/:id/cover、.../audio 移除附件;移除音频同时删除进度
GET /api/v1/books/:id/cover、.../audio 二进制响应,必须带会话鉴权,他人 404;用 http.ServeContent 自动支持 Range(206)、ETag/Last-Modified 与条件请求
PUT /api/v1/books/:id/playback {positionSeconds},upsert,仅本人
GET /api/v1/books、GET /api/v1/books/:id 增加封面版本号(用于缓存失效)与音频元数据(大小/是否有音频/上次位置)

D3 校验:音频 ≤ 20 MiB、封面 ≤ 2 MiB、封面像素 ≤ 4096×4096;按文件头 magic bytes 判定类型(MP3:ID3 或 0xFFEx 帧同步;JPG/PNG/WebP 各自签名),扩展名不参与判定;超限 413,类型/内容非法 400 且消息可读;不用文件名做路径。

D4 权限与清理:附件属于书籍所有者,任何跨账号读写返回 404;删除书籍级联删除附件与所有账号的该书进度;替换失败不写库(先校验后事务 upsert),旧附件保留。

D5 播放进度:只做上界校验(0 ≤ 秒 ≤ 24h)与 upsert;并发以最后一次写入为准;不解析音频时长(时长由浏览器提供,仅用于界面)。

D6 界面:实现 S1–S4;桌面与 390×844 触摸视口都要可用,不遮挡正文与划词面板。

D7 #15 衔接:附件存 MySQL,因此现有 dump 自动包含附件与进度;完成后重跑 #15 的恢复演练并新增附件断言(附件行数、字节 sha256 一致,恢复后仍可播放/读取),部署页与运维页补「备份含附件及其体积影响」。

D8 范围外:多音轨/分章音轨、字幕与文本同步、TTS、转码、自动搜索封面、公开分享、跨用户附件共享、时长解析。首版音频只收 MP3(最终支持以桌面与手机浏览器实测为准,不承诺任意格式)。

D9 测试:Go 单测(magic bytes、上限、Range 头、进度 upsert/幂等/重置、级联)、MySQL 集成(上传→读取→Range 206→条件请求→替换→移除→删除书籍级联)、前端单测与 e2e(mock + 390×844)、真实链路检查、迁移 v7→v8 与「回退标记后重新升级」。

D10 文档:Architecture、Business-Rules、Local-Development、Product-Requirements、Home,以及 Deployment-and-Operations 的备份范围与容量提示。

D11 回退:schema v8 只新增两张表;回退二进制前把 lexgo_schema.version 写回 7 即可,不影响既有数据。

D12 已知限制(写入文档):音频不做转码,浏览器兼容性以实测为准;把音频放进 MySQL 适合少量邀请用户的实例,不适合大规模音频库(单文件上限 20 MiB 已写明)。

三、请用户确认

  1. 上面的线框(S1–S4)与状态规则是否可以直接编码?如需调整(例如播放器条改到正文下方、封面比例改为 3:4),请指出。
  2. 若确认,本单置为「进行中」并开始实施;设计证据按本评论冻结,后续若交互变化会重新确认。
## #21 设计证据(形式 b:标注线框 + 状态说明)与契约草案(2026-09-15) 用户 2026-09-15 确认「做 #21,按建议的默认 + 用 (b) 设计证据」。本评论先交付**可审阅的设计证据**与实施契约;按 #21 正文的门禁,**新增交互需用户确认后编码**,因此工单在本评论后置为「待确认」,用户确认即开始实施。 ### 一、设计证据(标注线框) 复用现有规范与布局,不新增配色、不新增页面级导航。 **S1 书库卡片(LibraryView,桌面/手机同构)** ``` ┌─────────────────────────┐ │ ┌─────────────────────┐ │ ← 封面 16:9,未设置=默认封面(淡色底 + 书名首字) │ │ 封面图 │ │ 上传后立即生效,无需刷新 │ └─────────────────────┘ │ │ Fictional reader │ ← 现有 .book-title │ 已就绪 · 3 章 │ ← 现有状态摘要 └─────────────────────────┘ ``` 手机:单列、封面随宽度自适应,不产生横向滚动。 **S2 书籍页「音频与封面」区块(BookView,位于现有 `.page-actions` 下方;不用对话框)** ``` 音频与封面 ┌────────────────────────────────────────────────────────┐ │ 封面 [预览 160×90] 已设置 · JPG · 1.2 MB │ │ [替换封面] [移除] │ │ 音频 MP3 · 3.4 MB 上次播放位置 12:30 │ │ [上传音频] [替换] [移除] │ └────────────────────────────────────────────────────────┘ ``` - 未设置时显示「未设置」+ 上传入口;不显示空的预览框。 - 进行中:按钮 loading + 「正在上传…」;失败:区块内可读错误(格式/大小/网络),**保留原附件**。 - 移除:二次确认(复用删除章节的确认样式)。 - 手机:上下堆叠,按钮全宽。 **S3 阅读页播放器条(ReaderView;仅当该书有音频时出现)** ``` ┌──────────────────────────────────────────────────────────┐ │ ▶/⏸ 0:12 ──────●──────────── 12:30 1.0× ↻ 重试 │ └──────────────────────────────────────────────────────────┘ 章节标题 正文… ``` - 位置:章节标题下方、正文上方;`position: sticky; top: 0`(手机同样 sticky)。 - 控件:播放/暂停、进度(可拖动,原生 range,方向键可用)、倍速 0.75/1.0/1.25/1.5。 - **无音频时不显示任何占位**;加载失败时条内显示「音频加载失败 · 重试」。 - 不自动播放;不与正文做任何同步或高亮。 **S4 状态与持久化规则** | 状态 | 行为 | |---|---| | 打开书 | 只恢复进度显示,不自动播放 | | 播放中 | 每 5 秒、暂停时、离开页面时上报位置(同一账号同一本书) | | 替换音频 | 旧进度重置为 0,避免套用到新文件 | | 移除音频 | 删除进度记录,播放器条消失 | | 其他账号 | 看不到附件播放位置,也不能读取附件(404) | ### 二、实施契约 **D1 数据(schema v8,新表,可重放 DDL)** - `lexgo_book_attachments`:`book_id` + `kind('audio'|'cover')` 唯一,`owner_id`、`mime`、`byte_size`、`sha256`、`bytes MEDIUMBLOB`、时间戳;外键级联到书籍与账号。一本书最多一段音频、一张封面。 - `lexgo_playback_positions`:主键 `(owner_id, book_id)`,`position_seconds`、`updated_at`;外键级联到账号与书籍。 - 沿用「只加新表、不用 `ALTER TABLE`」的既定做法,迁移与回退测试照 #13。 **D2 接口** | 接口 | 说明 | |---|---| | `POST /api/v1/books/:id/cover`、`POST /api/v1/books/:id/audio` | multipart 单文件;成功返回书籍附件元数据;替换即覆盖 | | `DELETE /api/v1/books/:id/cover`、`.../audio` | 移除附件;移除音频同时删除进度 | | `GET /api/v1/books/:id/cover`、`.../audio` | 二进制响应,**必须带会话鉴权**,他人 404;用 `http.ServeContent` 自动支持 **Range(206)**、`ETag`/`Last-Modified` 与条件请求 | | `PUT /api/v1/books/:id/playback` | `{positionSeconds}`,upsert,仅本人 | | `GET /api/v1/books`、`GET /api/v1/books/:id` | 增加封面版本号(用于缓存失效)与音频元数据(大小/是否有音频/上次位置) | **D3 校验**:音频 ≤ 20 MiB、封面 ≤ 2 MiB、封面像素 ≤ 4096×4096;**按文件头 magic bytes 判定类型**(MP3:ID3 或 0xFFEx 帧同步;JPG/PNG/WebP 各自签名),扩展名不参与判定;超限 413,类型/内容非法 400 且消息可读;不用文件名做路径。 **D4 权限与清理**:附件属于书籍所有者,任何跨账号读写返回 404;删除书籍级联删除附件与所有账号的该书进度;替换失败不写库(先校验后事务 upsert),旧附件保留。 **D5 播放进度**:只做上界校验(0 ≤ 秒 ≤ 24h)与 upsert;并发以最后一次写入为准;不解析音频时长(时长由浏览器提供,仅用于界面)。 **D6 界面**:实现 S1–S4;桌面与 390×844 触摸视口都要可用,不遮挡正文与划词面板。 **D7 #15 衔接**:附件存 MySQL,因此现有 dump 自动包含附件与进度;完成后**重跑 #15 的恢复演练并新增附件断言**(附件行数、字节 sha256 一致,恢复后仍可播放/读取),部署页与运维页补「备份含附件及其体积影响」。 **D8 范围外**:多音轨/分章音轨、字幕与文本同步、TTS、转码、自动搜索封面、公开分享、跨用户附件共享、时长解析。首版音频只收 MP3(最终支持以桌面与手机浏览器实测为准,不承诺任意格式)。 **D9 测试**:Go 单测(magic bytes、上限、Range 头、进度 upsert/幂等/重置、级联)、MySQL 集成(上传→读取→Range 206→条件请求→替换→移除→删除书籍级联)、前端单测与 e2e(mock + 390×844)、真实链路检查、迁移 v7→v8 与「回退标记后重新升级」。 **D10 文档**:Architecture、Business-Rules、Local-Development、Product-Requirements、Home,以及 Deployment-and-Operations 的备份范围与容量提示。 **D11 回退**:schema v8 只新增两张表;回退二进制前把 `lexgo_schema.version` 写回 7 即可,不影响既有数据。 **D12 已知限制(写入文档)**:音频不做转码,浏览器兼容性以实测为准;把音频放进 MySQL 适合少量邀请用户的实例,不适合大规模音频库(单文件上限 20 MiB 已写明)。 ### 三、请用户确认 1. 上面的线框(S1–S4)与状态规则是否可以直接编码?如需调整(例如播放器条改到正文下方、封面比例改为 3:4),请指出。 2. 若确认,本单置为「进行中」并开始实施;设计证据按本评论冻结,后续若交互变化会重新确认。
Author
Owner

#21 实施启动(2026-09-15)

用户 2026-09-15 确认「可以」,并授权一并修掉 e2e/phrase.spec.ts 的偶发失败。设计证据按评论 8116 冻结(线框 S1–S4 与状态规则),契约 D1–D12 不变。分支 feat/21-book-attachments 从 main dde2ac1 创建。

实施顺序:

  1. 先修 e2e/phrase.spec.ts:63 的偶发失败(#11 遗留,与 #21 无关但会干扰后续全量回归);
  2. 后端:schema v8(lexgo_book_attachments、lexgo_playback_positions)、上传/替换/移除、带 Range 的受鉴权读取、播放位置上报表、书籍列表与详情返回封面版本与音频元数据;
  3. 前端:书库封面(S1)、书籍页附件区块(S2)、阅读页播放器条(S3)、位置上报与重置(S4);
  4. 测试与证据:Go 单测与 MySQL 集成、前端单测与 e2e(含 390×844 触摸视口)、真实链路检查、重跑 #15 恢复演练并加附件断言;
  5. 文档:Architecture、Business-Rules、Local-Development、Product-Requirements、Home、Deployment-and-Operations(备份含附件与体积提示)。

工单转「进行中」,完成后停在待用户验收。

## #21 实施启动(2026-09-15) 用户 2026-09-15 确认「可以」,并授权一并修掉 `e2e/phrase.spec.ts` 的偶发失败。设计证据按评论 [8116](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8116) 冻结(线框 S1–S4 与状态规则),契约 D1–D12 不变。分支 `feat/21-book-attachments` 从 main `dde2ac1` 创建。 实施顺序: 1. 先修 `e2e/phrase.spec.ts:63` 的偶发失败(#11 遗留,与 #21 无关但会干扰后续全量回归); 2. 后端:schema v8(`lexgo_book_attachments`、`lexgo_playback_positions`)、上传/替换/移除、带 Range 的受鉴权读取、播放位置上报表、书籍列表与详情返回封面版本与音频元数据; 3. 前端:书库封面(S1)、书籍页附件区块(S2)、阅读页播放器条(S3)、位置上报与重置(S4); 4. 测试与证据:Go 单测与 MySQL 集成、前端单测与 e2e(含 390×844 触摸视口)、真实链路检查、**重跑 #15 恢复演练并加附件断言**; 5. 文档:Architecture、Business-Rules、Local-Development、Product-Requirements、Home、Deployment-and-Operations(备份含附件与体积提示)。 工单转「进行中」,完成后停在待用户验收。
Author
Owner

#21 实施完成,待用户验收(2026-09-15)

用户 2026-09-15 确认设计证据(评论 8116 的线框 S1–S4 与状态规则)并授权一并修掉 e2e/phrase.spec.ts 的偶发失败。分支 feat/21-book-attachments 从 main dde2ac1 创建,提交 ab23ff6 已推送;PR #36 未合并,工单停在待用户验收。

实现与差异

  • schema v8(两张新表,可重放 DDL,无 ALTER TABLE):lexgo_book_attachments(主键 book_id+kind,bytes MEDIUMBLOB,外键级联到书籍与账号)、lexgo_playback_positions(主键 owner_id+book_id,级联到账号与书籍)。
  • 后端 server/app/lexgo/attachment.go:按文件头 magic bytes 判定类型(MP3 接受 ID3 或帧同步;JPG/PNG/WebP 各自签名,WebP 解析 VP8X/VP8/VP8L 头取维度);音频 ≤20 MiB、封面 ≤2 MiB 且 ≤4096×4096;先校验后 upsert,被拒绝的上传不动旧文件;替换或移除音频即清空位置。读取端点通过新增的 binaryResponse 交给 http.ServeContent,因此支持 Range/206、416、If-Modified-Since 与 ETag/304;书籍列表新增 coverVersion/hasAudio,书籍详情与章节响应带 attachments。
  • 学习端:session.requestBlob 带凭据取字节并转对象 URL(令牌不进 URL),库列表批量预取封面、阅读器按需取音频;AudioPlayer.vue(播放/暂停、拖动、0.75–1.5 倍速、错误重试、每 5 秒与暂停/离开上报位置);书库卡片封面(重复链接对无障碍隐藏,标题链接仍是唯一入口)、书籍页「音频与封面」区块、阅读页 sticky 播放器条。
  • 修掉 e2e/phrase.spec.ts 的偶发失败:重试时重新读取词坐标(面板打开会改变布局)、松开前留 80 ms 让浏览器扩展选区、重试条件是断言的期望词数而不是「面板出现了」、重试前关闭部分选中的面板。

验证

验证 结果
go vet / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 86 项顶层用例通过、0 跳过(新增 7 项:类型嗅探、上限与像素、WebP 维度、位置边界、附件集成用例、v7→v8 迁移与回退重升级)
learner vitest / vue-tsc / build / playwright test 157 项单测、类型检查、构建、26 项 E2E(桌面 22 + 移动 4);连续 3 次全量跑全部通过(含原先偶发失败的用例)
admin 31 项与 lint 通过(无改动)
治理 / 镜像 65 项、check --strict、sync --check 通过
真实 API+MySQL 41 项检查通过:真实 PNG(461 B)与 ffmpeg 生成的三秒 MP3(18 407 B)上传后逐字节比对、OCTET_LENGTH 与 sha256 一致、ETag/304、Range 得 206 与正确切片、不可满足 Range 得 416、越权与匿名被拒、SVG/文本改名被拒、21 MiB 得 413 且旧文件保留、位置读写与负值拒绝、替换后位置归零且仍一行、移除与删书级联清空
#15 恢复演练(含附件) 22 项检查通过:Go 二进制备份(manifest 已含两张新表)→ 恢复到空库 → lexgo verify --manifest 通过(含新增的「附件都指向存在的书」等检查)→ 15 张表逐表内容校验和与源库一致 → 用恢复库起实例:同一账号可登录、书籍仍报告两个附件、位置仍是原值、音频与封面按字节相同地返回、Range 仍得 206、越权仍 404

迁移:v7 → v8 只新增两张表,开发库逐表业务计数无变化,schema 记到 8;本机 lexgo-api 已用新二进制重启,回退二进制 .local/lexgo-pre-issue21.exe。

未验证(如实记录)

  • 浏览器是否真实解码播放未验证:E2E 中媒体元素被桩替换(合成帧不足以证明解码),因此真实 MP3 的听感、手机扬声器/蓝牙、后台播放与锁屏控制需要人工确认。
  • 未验证 Safari/Firefox;WebP 只验证了维度解析,未在各浏览器实测显示。
  • 20 MiB 上限下的播放启动延迟未测;附件对 dump 体积与恢复耗时的影响只在当前小数据量下观察。
  • 前端未做渐进式流式播放(浏览器整份取回后播放);服务端的 Range 已实现并有测试,供带 Authorization 头的客户端使用。
  • 真机触摸/听感缺口仍保留(#4 起的既有缺口)。

文档

  • Architecture-and-Code-Map: 3b785a11352776e36656da59ef9928a2cbb42694
  • Business-Rules-and-Glossary: 34a1994dae20957a68f2bd60e3a7dd97530cffac
  • Local-Development-and-Verification: ba5e3acc746432e55c186fd3a1fdf3c158fd121b
  • Product-Requirements-Overview: 437d154868c23dd5b39e525d6c45e9ff13364d47
  • Home: e1e363e39ea02c26fab7f96c696ee20e60dcec60
  • Deployment-and-Operations: dacdad8a9385b2fde153a0dd1fe2579b57a810fd(备份范围含附件与体积提示)
  • 镜像校验:sync --check 通过,6 个变更页逐字节正文比对一致

回退

schema v8 只新增两张表:停止 lexgo-api、把 lexgo_schema.version 写回 7、恢复 .local/lexgo-pre-issue21.exe 后重启即可;既有书籍与学习数据不受影响。

Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。

证据文件(本机,不入仓库):.local/issue21-api-evidence.json(41 项)、.local/issue21-restore-evidence.json(22 项)、fixture 的真实 MP3 与 PNG(.local/evidence/issue21-fixture.*)。

## #21 实施完成,待用户验收(2026-09-15) 用户 2026-09-15 确认设计证据(评论 [8116](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8116) 的线框 S1–S4 与状态规则)并授权一并修掉 `e2e/phrase.spec.ts` 的偶发失败。分支 `feat/21-book-attachments` 从 main `dde2ac1` 创建,提交 `ab23ff6` 已推送;PR [#36](https://git.ilapage.cn/OPC/lexgo/pulls/36) 未合并,工单停在待用户验收。 ### 实现与差异 - **schema v8**(两张新表,可重放 DDL,无 `ALTER TABLE`):`lexgo_book_attachments`(主键 `book_id+kind`,`bytes MEDIUMBLOB`,外键级联到书籍与账号)、`lexgo_playback_positions`(主键 `owner_id+book_id`,级联到账号与书籍)。 - **后端** `server/app/lexgo/attachment.go`:按**文件头 magic bytes** 判定类型(MP3 接受 ID3 或帧同步;JPG/PNG/WebP 各自签名,WebP 解析 VP8X/VP8/VP8L 头取维度);音频 ≤20 MiB、封面 ≤2 MiB 且 ≤4096×4096;**先校验后 upsert**,被拒绝的上传不动旧文件;替换或移除音频即清空位置。读取端点通过新增的 `binaryResponse` 交给 `http.ServeContent`,因此支持 **Range/206、416、If-Modified-Since 与 ETag/304**;书籍列表新增 `coverVersion`/`hasAudio`,书籍详情与章节响应带 `attachments`。 - **学习端**:`session.requestBlob` 带凭据取字节并转对象 URL(**令牌不进 URL**),库列表批量预取封面、阅读器按需取音频;`AudioPlayer.vue`(播放/暂停、拖动、0.75–1.5 倍速、错误重试、每 5 秒与暂停/离开上报位置);书库卡片封面(重复链接对无障碍隐藏,标题链接仍是唯一入口)、书籍页「音频与封面」区块、阅读页 sticky 播放器条。 - **修掉 `e2e/phrase.spec.ts` 的偶发失败**:重试时重新读取词坐标(面板打开会改变布局)、松开前留 80 ms 让浏览器扩展选区、**重试条件是断言的期望词数**而不是「面板出现了」、重试前关闭部分选中的面板。 ### 验证 | 验证 | 结果 | |---|---| | `go vet` / `gofmt -l` | 通过、无输出 | | `LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration` | **86 项顶层用例通过、0 跳过**(新增 7 项:类型嗅探、上限与像素、WebP 维度、位置边界、附件集成用例、v7→v8 迁移与回退重升级) | | learner `vitest` / `vue-tsc` / `build` / `playwright test` | 157 项单测、类型检查、构建、**26 项 E2E**(桌面 22 + 移动 4);连续 3 次全量跑全部通过(含原先偶发失败的用例) | | admin | 31 项与 lint 通过(无改动) | | 治理 / 镜像 | 65 项、`check --strict`、`sync --check` 通过 | | 真实 API+MySQL | **41 项检查通过**:真实 PNG(461 B)与 ffmpeg 生成的三秒 MP3(18 407 B)上传后逐字节比对、`OCTET_LENGTH` 与 sha256 一致、ETag/304、`Range` 得 206 与正确切片、不可满足 Range 得 416、越权与匿名被拒、SVG/文本改名被拒、21 MiB 得 413 且旧文件保留、位置读写与负值拒绝、替换后位置归零且仍一行、移除与删书级联清空 | | **#15 恢复演练(含附件)** | **22 项检查通过**:Go 二进制备份(manifest 已含两张新表)→ 恢复到空库 → `lexgo verify --manifest` 通过(含新增的「附件都指向存在的书」等检查)→ **15 张表逐表内容校验和与源库一致** → 用恢复库起实例:同一账号可登录、书籍仍报告两个附件、位置仍是原值、**音频与封面按字节相同地返回**、Range 仍得 206、越权仍 404 | 迁移:v7 → v8 只新增两张表,开发库逐表业务计数无变化,schema 记到 8;本机 lexgo-api 已用新二进制重启,回退二进制 `.local/lexgo-pre-issue21.exe`。 ### 未验证(如实记录) - **浏览器是否真实解码播放未验证**:E2E 中媒体元素被桩替换(合成帧不足以证明解码),因此真实 MP3 的听感、手机扬声器/蓝牙、后台播放与锁屏控制需要人工确认。 - 未验证 Safari/Firefox;WebP 只验证了维度解析,未在各浏览器实测显示。 - 20 MiB 上限下的播放启动延迟未测;附件对 dump 体积与恢复耗时的影响只在当前小数据量下观察。 - 前端未做渐进式流式播放(浏览器整份取回后播放);服务端的 Range 已实现并有测试,供带 Authorization 头的客户端使用。 - 真机触摸/听感缺口仍保留(#4 起的既有缺口)。 ### 文档 - Architecture-and-Code-Map: `3b785a11352776e36656da59ef9928a2cbb42694` - Business-Rules-and-Glossary: `34a1994dae20957a68f2bd60e3a7dd97530cffac` - Local-Development-and-Verification: `ba5e3acc746432e55c186fd3a1fdf3c158fd121b` - Product-Requirements-Overview: `437d154868c23dd5b39e525d6c45e9ff13364d47` - Home: `e1e363e39ea02c26fab7f96c696ee20e60dcec60` - Deployment-and-Operations: `dacdad8a9385b2fde153a0dd1fe2579b57a810fd`(备份范围含附件与体积提示) - 镜像校验:`sync --check` 通过,6 个变更页逐字节正文比对一致 ### 回退 schema v8 只新增两张表:停止 lexgo-api、把 `lexgo_schema.version` 写回 7、恢复 `.local/lexgo-pre-issue21.exe` 后重启即可;既有书籍与学习数据不受影响。 Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。 证据文件(本机,不入仓库):`.local/issue21-api-evidence.json`(41 项)、`.local/issue21-restore-evidence.json`(22 项)、fixture 的真实 MP3 与 PNG(`.local/evidence/issue21-fixture.*`)。
ila changed title from MVP 增补:书籍音频与封面附件(音频不与文本同步) to MVP 增补:书籍封面、章节音频与章节插图(音频不与文本同步) 2026-09-15 19:37:30 +08:00
Author
Owner

范围变更记录(2026-09-15)

触发场景

用户提出实际使用场景:"有个场景,我收集了多篇网络文章,每篇有自己的音频,要怎样来实现"。经讨论确认组织方式为"多篇文章合并成一本书的多章",因此音频挂载单位需要从建单时确认的"每本书一个"改为"每个章节一个",播放进度记忆粒度同步从"账号+书"改为"账号+章节"。

讨论中进一步确认新增插图需求,用户原话:"书籍封面只有一个,我想给每章节加个图片,当插图或章节图片,增加阅读氛围和兴趣"。确认为:一章最多一张插图,固定展示在该章正文开头,可复用为书籍详情页章节列表的缩略图;不支持正文中间图文混排、不支持多图。

封面维持书级不变——用户明确认可"封面仍然按书"的建议。

变更内容

项目 2026-09-10 建单时 2026-09-15 更新后
封面挂载单位 书 书(不变)
音频挂载单位 书 章节
播放进度粒度 账号+书 账号+章节
插图 无 新增:章节级,一章一张,固定于正文开头
附件类型数 2(封面、音频) 3(封面、音频、插图)
估算 4~6 人日 5~8 人日

影响范围

  • 已在工单正文的"已确认首版范围""验收标准""设计证据与实现前检查""估算、风险与回退"各节同步更新,标注变更时间与原因。
  • 尚未产生任何代码或原型投入(状态仍为"待实施"),本次变更成本低,不涉及返工。
  • 下一步仍需先更新原型(音频/插图上传入口、章节列表插图展示、阅读页播放器位置)并经用户确认,才能进入正式设计与建单实施;本次只更新工单范围,不启动编码。

Gitea MCP 仍指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅从既有安全配置读入进程。

## 范围变更记录(2026-09-15) ### 触发场景 用户提出实际使用场景:"有个场景,我收集了多篇网络文章,每篇有自己的音频,要怎样来实现"。经讨论确认组织方式为"多篇文章合并成一本书的多章",因此音频挂载单位需要从建单时确认的"每本书一个"改为"每个章节一个",播放进度记忆粒度同步从"账号+书"改为"账号+章节"。 讨论中进一步确认新增插图需求,用户原话:"书籍封面只有一个,我想给每章节加个图片,当插图或章节图片,增加阅读氛围和兴趣"。确认为:一章最多一张插图,固定展示在该章正文开头,可复用为书籍详情页章节列表的缩略图;不支持正文中间图文混排、不支持多图。 封面维持书级不变——用户明确认可"封面仍然按书"的建议。 ### 变更内容 | 项目 | 2026-09-10 建单时 | 2026-09-15 更新后 | |---|---|---| | 封面挂载单位 | 书 | 书(不变) | | 音频挂载单位 | 书 | **章节** | | 播放进度粒度 | 账号+书 | **账号+章节** | | 插图 | 无 | **新增:章节级,一章一张,固定于正文开头** | | 附件类型数 | 2(封面、音频) | 3(封面、音频、插图) | | 估算 | 4~6 人日 | 5~8 人日 | ### 影响范围 - 已在工单正文的"已确认首版范围""验收标准""设计证据与实现前检查""估算、风险与回退"各节同步更新,标注变更时间与原因。 - 尚未产生任何代码或原型投入(状态仍为"待实施"),本次变更成本低,不涉及返工。 - 下一步仍需先更新原型(音频/插图上传入口、章节列表插图展示、阅读页播放器位置)并经用户确认,才能进入正式设计与建单实施;本次只更新工单范围,不启动编码。 Gitea MCP 仍指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅从既有安全配置读入进程。
Author
Owner

#21 范围变更(第二次,草案,待确认)——2026-09-15

背景

PR #36(提交 ab23ff6)已按评论 8116 冻结的 D1~D12 完成书级音频与封面,86 项集成用例、真实链路 41 项、#15 恢复演练 22 项全部通过,目前停在待验收,尚未验收关闭。

用户随后提出实际场景(多篇网络文章合并成一本书的多章,每篇/每章各有自己的音频,并新增章节插图需求),与已交付的书级方案不一致。用户已确认:在 PR #36 已交付的代码基础上继续改,不推倒重做,本评论是这次改动的完整契约草案,供确认后再实施。

一、复用与保留(不改动)

  • 封面维持书级,lexgo_book_attachments 的 kind='cover'、书籍读写接口、coverVersion 缓存失效机制原样保留。
  • magic bytes 类型嗅探、http.ServeContent 的 Range/ETag/条件请求包装、上传校验流程(先校验后事务写入、失败保留旧附件)、AudioPlayer.vue、session.requestBlob 取字节转对象 URL 的机制,全部原样复用到章级。

二、退役(书级音频整体下线)

  • lexgo_book_attachments 的 kind='audio' 不再接受新写入;POST/DELETE/GET /api/v1/books/:id/audio 与 PUT /api/v1/books/:id/playback 三个接口连同 lexgo_playback_positions 表一并退役。
  • 产品尚未邀请真实用户(自托管、MVP 未上线),v8 期间可能产生的书级音频测试数据直接清空,不做存量迁移。

三、实施契约(D1~D12,覆盖第一版对应条目)

D1 数据(schema v9,新表,可重放 DDL,不用 ALTER TABLE)

  • lexgo_chapter_attachments:chapter_id + kind('audio'|'illustration') 唯一,owner_id、mime、byte_size、sha256、bytes MEDIUMBLOB、时间戳;外键级联到 lexgo_chapters(id) 与账号。一章最多一段音频、一张插图。
  • lexgo_chapter_playback_positions:主键 (owner_id, chapter_id),position_seconds、updated_at;外键级联到账号与章节。
  • v9 迁移内额外执行一条数据清理语句:DELETE FROM lexgo_book_attachments WHERE kind = 'audio'(幂等、可重放),lexgo_playback_positions 表整体保留但停止写入(不删表,避免破坏"只加不删"的迁移习惯;如需彻底清理留待后续单独评估)。

D2 接口

接口 说明
POST /api/v1/chapters/:id/audio、.../illustration multipart 单文件,替换即覆盖
DELETE /api/v1/chapters/:id/audio、.../illustration 移除;移除音频同时删除该章播放位置
GET /api/v1/chapters/:id/audio、.../illustration 二进制响应,会话鉴权,他人 404,http.ServeContent 提供 Range/206、ETag/304
PUT /api/v1/chapters/:id/playback {positionSeconds},upsert,仅本人
GET /api/v1/books/:id(章节列表) 每章增加插图缩略图版本号、是否有音频、上次播放位置
POST/DELETE/GET /api/v1/books/:id/audio 退役
PUT /api/v1/books/:id/playback 退役

D3 校验:音频规则不变(≤20 MiB、MP3 magic bytes);插图与封面共用同一套校验(JPG/PNG/WebP、≤2 MiB、≤4096×4096、magic bytes 判定,不看扩展名)。

D4 权限与清理:附件属于章节所有者,跨账号读写 404;删除章节(复用 #10 已确认的事务级联)连带删除该章附件与播放位置;删除书籍通过章节级联连带清空。

D5 播放进度:粒度改为账号+章节;规则不变(0~24h 上界、最后写入为准、不解析时长)。

D6 界面(改写 S2,新增 S2b,S1/S3/S4 结构不变、归属改为章级)

  • S1 书库卡片:不变(封面仍书级)。
  • S2 书籍页:只保留"封面"部分(复用 D1 已冻结的样式),移除"音频"行——书级音频区块整体删除。
  • S2b 章节列表行(新增,位于 #10 已有的章节列表/编辑入口):每行增加插图缩略图(无插图显示占位)与"音频 · 插图"编辑入口;编辑对话框内分别提供插图与音频的上传/替换/移除,样式复用 S2 已确认的"预览+已设置状态+操作按钮"排版。
  • S3 阅读页:播放器条改为该章有音频才出现(不再是"该书有音频"),位置不变(章节标题下方、正文上方、sticky);插图展示在正文最前面(章节标题与播放器条之下、正文之上),无插图不占位。
  • S4 状态与持久化:把"账号+本书"全部改成"账号+本章";新增"离开当前章节/切换到其他章节"时上报一次位置。
  • 手机(390×844):插图与音频编辑入口在章节列表行内可点开,不产生横向溢出。

D7 #15 衔接:#15 恢复演练需要第三次重跑(第一次覆盖数据库,第二次加书级附件断言,这次改成章级附件断言:附件行数、按章节归属、sha256 一致、恢复后仍可按章播放/显示插图)。

D8 范围外:与第一版一致,另加"文章缩略图墙"式独立浏览界面明确排除(已写入工单正文非目标)。

D9 测试:在 v8 已有用例基础上,新增插图类型/尺寸校验、章节删除级联清空章级附件与位置(复用 #10 的删除测试夹具)、v8→v9 迁移与"回退标记后重新升级"、书级音频接口确认已下线(404 或路由不存在)、书级历史音频行被迁移清空。

D10 文档:Architecture、Business-Rules、Local-Development、Product-Requirements、Home、Deployment-and-Operations 均需修正为章级口径,删除书级音频的描述。

D11 回退:v9 只新增两张表:回退到 v8 二进制会重新看到(已清空的)书级音频接口,但因为迁移已经清空了 kind='audio' 的历史行,回退到 v8 无法找回这批测试期数据——鉴于目前只有测试数据、没有真实用户,这个代价可以接受,工单会明确记录这一点。

D12 已知限制(写入文档):本单是 #21 的第二次范围变更,第一次交付的书级音频功能整体作废,相关实现与测试投入不再产生用户可见价值;后续如需再评估附件挂载粒度,应先确认真实使用场景,避免第三次返工。

三、请用户确认

  1. 上述 D1~D12(尤其 D6 的 S2/S2b/S3/S4 界面改写)是否可以直接在 PR #36 分支上继续实施?
  2. 是否接受"书级音频测试数据清空、不做存量迁移"这一处理方式?
  3. 确认后工单转回"进行中",在现有分支继续提交,完成后仍停在待验收。
## #21 范围变更(第二次,草案,待确认)——2026-09-15 ### 背景 PR #36(提交 `ab23ff6`)已按评论 [8116](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8116) 冻结的 D1~D12 完成**书级**音频与封面,86 项集成用例、真实链路 41 项、#15 恢复演练 22 项全部通过,目前停在待验收,**尚未验收关闭**。 用户随后提出实际场景(多篇网络文章合并成一本书的多章,每篇/每章各有自己的音频,并新增章节插图需求),与已交付的书级方案不一致。用户已确认:**在 PR #36 已交付的代码基础上继续改,不推倒重做**,本评论是这次改动的完整契约草案,供确认后再实施。 ### 一、复用与保留(不改动) - 封面维持**书级**,`lexgo_book_attachments` 的 `kind='cover'`、书籍读写接口、`coverVersion` 缓存失效机制**原样保留**。 - magic bytes 类型嗅探、`http.ServeContent` 的 Range/ETag/条件请求包装、上传校验流程(先校验后事务写入、失败保留旧附件)、`AudioPlayer.vue`、`session.requestBlob` 取字节转对象 URL 的机制,全部原样复用到章级。 ### 二、退役(书级音频整体下线) - `lexgo_book_attachments` 的 `kind='audio'` 不再接受新写入;`POST/DELETE/GET /api/v1/books/:id/audio` 与 `PUT /api/v1/books/:id/playback` 三个接口连同 `lexgo_playback_positions` 表一并退役。 - 产品尚未邀请真实用户(自托管、MVP 未上线),v8 期间可能产生的书级音频测试数据直接清空,不做存量迁移。 ### 三、实施契约(D1~D12,覆盖第一版对应条目) **D1 数据(schema v9,新表,可重放 DDL,不用 ALTER TABLE)** - `lexgo_chapter_attachments`:`chapter_id` + `kind('audio'|'illustration')` 唯一,`owner_id`、`mime`、`byte_size`、`sha256`、`bytes MEDIUMBLOB`、时间戳;外键级联到 `lexgo_chapters(id)` 与账号。一章最多一段音频、一张插图。 - `lexgo_chapter_playback_positions`:主键 `(owner_id, chapter_id)`,`position_seconds`、`updated_at`;外键级联到账号与章节。 - v9 迁移内额外执行一条数据清理语句:`DELETE FROM lexgo_book_attachments WHERE kind = 'audio'`(幂等、可重放),`lexgo_playback_positions` 表整体保留但停止写入(不删表,避免破坏"只加不删"的迁移习惯;如需彻底清理留待后续单独评估)。 **D2 接口** | 接口 | 说明 | |---|---| | `POST /api/v1/chapters/:id/audio`、`.../illustration` | multipart 单文件,替换即覆盖 | | `DELETE /api/v1/chapters/:id/audio`、`.../illustration` | 移除;移除音频同时删除该章播放位置 | | `GET /api/v1/chapters/:id/audio`、`.../illustration` | 二进制响应,会话鉴权,他人 404,`http.ServeContent` 提供 Range/206、ETag/304 | | `PUT /api/v1/chapters/:id/playback` | `{positionSeconds}`,upsert,仅本人 | | `GET /api/v1/books/:id`(章节列表)| 每章增加插图缩略图版本号、是否有音频、上次播放位置 | | ~~`POST/DELETE/GET /api/v1/books/:id/audio`~~ | **退役** | | ~~`PUT /api/v1/books/:id/playback`~~ | **退役** | **D3 校验**:音频规则不变(≤20 MiB、MP3 magic bytes);插图与封面共用同一套校验(JPG/PNG/WebP、≤2 MiB、≤4096×4096、magic bytes 判定,不看扩展名)。 **D4 权限与清理**:附件属于章节所有者,跨账号读写 404;删除章节(复用 #10 已确认的事务级联)连带删除该章附件与播放位置;删除书籍通过章节级联连带清空。 **D5 播放进度**:粒度改为账号+章节;规则不变(0~24h 上界、最后写入为准、不解析时长)。 **D6 界面(改写 S2,新增 S2b,S1/S3/S4 结构不变、归属改为章级)** - **S1 书库卡片**:不变(封面仍书级)。 - **S2 书籍页**:只保留"封面"部分(复用 D1 已冻结的样式),移除"音频"行——书级音频区块整体删除。 - **S2b 章节列表行(新增,位于 #10 已有的章节列表/编辑入口)**:每行增加插图缩略图(无插图显示占位)与"音频 · 插图"编辑入口;编辑对话框内分别提供插图与音频的上传/替换/移除,样式复用 S2 已确认的"预览+已设置状态+操作按钮"排版。 - **S3 阅读页**:播放器条改为**该章有音频才出现**(不再是"该书有音频"),位置不变(章节标题下方、正文上方、sticky);插图展示在正文最前面(章节标题与播放器条之下、正文之上),无插图不占位。 - **S4 状态与持久化**:把"账号+本书"全部改成"账号+本章";新增"离开当前章节/切换到其他章节"时上报一次位置。 - 手机(390×844):插图与音频编辑入口在章节列表行内可点开,不产生横向溢出。 **D7 #15 衔接**:#15 恢复演练需要**第三次**重跑(第一次覆盖数据库,第二次加书级附件断言,这次改成章级附件断言:附件行数、按章节归属、sha256 一致、恢复后仍可按章播放/显示插图)。 **D8 范围外**:与第一版一致,另加"文章缩略图墙"式独立浏览界面明确排除(已写入工单正文非目标)。 **D9 测试**:在 v8 已有用例基础上,新增插图类型/尺寸校验、章节删除级联清空章级附件与位置(复用 #10 的删除测试夹具)、v8→v9 迁移与"回退标记后重新升级"、书级音频接口确认已下线(404 或路由不存在)、书级历史音频行被迁移清空。 **D10 文档**:Architecture、Business-Rules、Local-Development、Product-Requirements、Home、Deployment-and-Operations 均需修正为章级口径,删除书级音频的描述。 **D11 回退**:v9 只新增两张表:回退到 v8 二进制会重新看到(已清空的)书级音频接口,但因为迁移已经清空了 `kind='audio'` 的历史行,**回退到 v8 无法找回这批测试期数据**——鉴于目前只有测试数据、没有真实用户,这个代价可以接受,工单会明确记录这一点。 **D12 已知限制(写入文档)**:本单是 #21 的第二次范围变更,第一次交付的书级音频功能整体作废,相关实现与测试投入不再产生用户可见价值;后续如需再评估附件挂载粒度,应先确认真实使用场景,避免第三次返工。 ### 三、请用户确认 1. 上述 D1~D12(尤其 D6 的 S2/S2b/S3/S4 界面改写)是否可以直接在 PR #36 分支上继续实施? 2. 是否接受"书级音频测试数据清空、不做存量迁移"这一处理方式? 3. 确认后工单转回"进行中",在现有分支继续提交,完成后仍停在待验收。
ila changed title from MVP 增补:书籍封面、章节音频与章节插图(音频不与文本同步) to MVP 增补:书籍音频与封面附件(音频不与文本同步) 2026-09-15 20:16:00 +08:00
Author
Owner

更正说明(2026-09-15)

我在评论 8128 里把工单正文改成了"章级音频+章级插图",但那次修改基于的信息不完整——当时不知道 PR #36 已经按评论 8116 的书级方案实施完成(评论 8124),导致正文描述的范围和已交付代码不一致。

现已把工单标题与正文改回评论 8128 之前的书级版本,与当前已交付、停在待验收的代码状态保持一致。评论 8128 记录的讨论过程本身不撤销(保留作为历史记录),但其"已生效"的表述作废。

第二次范围变更(章级音频+插图)的完整草案见评论 8129,目前是待确认状态,尚未生效。用户确认后,我会再次更新正文并把工单转回"进行中"。

Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。

## 更正说明(2026-09-15) 我在评论 [8128](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8128) 里把工单正文改成了"章级音频+章级插图",但那次修改基于的信息不完整——当时不知道 PR #36 已经按评论 [8116](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8116) 的书级方案实施完成(评论 [8124](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8124)),导致正文描述的范围和已交付代码不一致。 现已把工单标题与正文**改回评论 8128 之前的书级版本**,与当前已交付、停在待验收的代码状态保持一致。评论 8128 记录的讨论过程本身不撤销(保留作为历史记录),但其"已生效"的表述作废。 第二次范围变更(章级音频+插图)的完整草案见评论 [8129](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8129),目前是**待确认**状态,尚未生效。用户确认后,我会再次更新正文并把工单转回"进行中"。 Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。
Author
Owner

#21 验收通过(2026-09-15)

用户 2026-09-15 回复「#21 验收通过」,随后确认「A,并新开章级工单」:接受本次交付的书级封面与音频,并把章级音频/章节插图的变更作为新工单承接。

  • 验收范围:设计证据与契约 D1~D12(评论 8116 的线框 S1–S4)— 书级封面 + 书级音频、书籍页附件区块、阅读页播放器、账号×书籍的播放位置。
  • 合并:PR #36 已 fast-forward-only 合入 main,merge commit ab23ff6。
  • 验证:Go 86 项顶层用例(含附件与 v7→v8 迁移)、学习端 157 单测与 26 项 E2E(连续 3 次全量跑通过,含修好的 e2e/phrase.spec.ts)、管理端 31 项、治理 65 项;真实 API+MySQL 41 项(真实 PNG 与 ffmpeg 生成的三秒 MP3,逐字节比对、Range 206/416、ETag 304、越权与校验拒绝、替换归零、级联清理);#15 恢复演练扩展 22 项,恢复后附件按字节相同地返回、Range 仍可用。
  • 文档 revision:Architecture 08472d2c98a5b4ac1b0354b09c6319ac74d321f7、Business-Rules 34a1994dae20957a68f2bd60e3a7dd97530cffac、Local-Development ba5e3acc746432e55c186fd3a1fdf3c158fd121b、Product-Requirements 2d6563574e807c9a167b90b7c772ee8b9b81e76d、Home eef77f39332c2bef01d5e9788a2c6c89796b2a1a、Deployment-and-Operations dacdad8a9385b2fde153a0dd1fe2579b57a810fd。
  • 未验证(保留):浏览器是否真实解码播放未验证(E2E 中媒体元素被桩替换),真机听感/兼容性待人工确认;Safari/Firefox 未测;前端为整份取回后播放(服务端 Range 已实现)。
  • 范围变更承接:用户提出的「多篇文章合并成一本书、每章各有音频,并新增章节插图」场景,按用户确认新开 #37 承接,草案来自本单评论 8129;#21 已交付的机制(magic bytes 嗅探、Range/ETag 包装、播放器组件、带凭据 fetch + 对象 URL)在 #37 中复用,不推倒重做。
  • 回退:schema v8 只新增两张表,写回 lexgo_schema.version=7 并恢复 .local/lexgo-pre-issue21.exe 即可。

单元工单关闭;#16 的 #21 复选框同步勾选。

## #21 验收通过(2026-09-15) 用户 2026-09-15 回复「#21 验收通过」,随后确认「**A,并新开章级工单**」:接受本次交付的**书级**封面与音频,并把章级音频/章节插图的变更作为新工单承接。 - 验收范围:设计证据与契约 D1~D12(评论 [8116](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8116) 的线框 S1–S4)— 书级封面 + 书级音频、书籍页附件区块、阅读页播放器、账号×书籍的播放位置。 - 合并:PR [#36](https://git.ilapage.cn/OPC/lexgo/pulls/36) 已 fast-forward-only 合入 main,merge commit `ab23ff6`。 - 验证:Go 86 项顶层用例(含附件与 v7→v8 迁移)、学习端 157 单测与 26 项 E2E(连续 3 次全量跑通过,含修好的 `e2e/phrase.spec.ts`)、管理端 31 项、治理 65 项;真实 API+MySQL 41 项(真实 PNG 与 ffmpeg 生成的三秒 MP3,逐字节比对、Range 206/416、ETag 304、越权与校验拒绝、替换归零、级联清理);**#15 恢复演练扩展 22 项**,恢复后附件按字节相同地返回、Range 仍可用。 - 文档 revision:Architecture `08472d2c98a5b4ac1b0354b09c6319ac74d321f7`、Business-Rules `34a1994dae20957a68f2bd60e3a7dd97530cffac`、Local-Development `ba5e3acc746432e55c186fd3a1fdf3c158fd121b`、Product-Requirements `2d6563574e807c9a167b90b7c772ee8b9b81e76d`、Home `eef77f39332c2bef01d5e9788a2c6c89796b2a1a`、Deployment-and-Operations `dacdad8a9385b2fde153a0dd1fe2579b57a810fd`。 - 未验证(保留):**浏览器是否真实解码播放未验证**(E2E 中媒体元素被桩替换),真机听感/兼容性待人工确认;Safari/Firefox 未测;前端为整份取回后播放(服务端 Range 已实现)。 - 范围变更承接:用户提出的「多篇文章合并成一本书、每章各有音频,并新增章节插图」场景,按用户确认**新开 #37** 承接,草案来自本单评论 [8129](https://git.ilapage.cn/OPC/lexgo/issues/21#issuecomment-8129);#21 已交付的机制(magic bytes 嗅探、Range/ETag 包装、播放器组件、带凭据 fetch + 对象 URL)在 #37 中复用,不推倒重做。 - 回退:schema v8 只新增两张表,写回 `lexgo_schema.version=7` 并恢复 `.local/lexgo-pre-issue21.exe` 即可。 单元工单关闭;#16 的 #21 复选框同步勾选。
ila closed this issue 2026-09-15 20:26:13 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/lexgo#21