42
Local-Development-and-Verification
ila edited this page 2026-09-16 21:01:55 +08:00
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.

本地开发与验证

环境要求

当前工作区为 Windows PowerShell,文档工具使用 Python 3。产品 Go、Node、MySQL 8 小版本、Redis、NLP 和模型版本在 M0 锁定,尚无产品运行环境。

Windows PowerShell 与 UTF-8

读中文使用 Get-Content -Encoding utf8,复杂路径使用 -LiteralPath。Python 输出可设置进程 PYTHONIOENCODING=utf-8;文件解码与终端显示分别处理。不得打印凭据。

PowerShell 语法与外部命令

不要套用 Bash heredoc;Python 脚本可用单引号 here-string。通过 $LASTEXITCODE 检查外部命令,使用原生 PowerShell 路径操作,避免跨 shell 拼接。

第一次运行

从仓库根目录执行:

命令 预期与边界
python dev_scripts/harness.py --help 退出码 0,列出工具命令
python dev_scripts/harness.py check --strict 退出码 0,正式主题结构与镜像元数据完整;不单独证明线上状态
python dev_scripts/harness.py sync --verify 从配置目标读取并同步全部映射页面,再严格检查,预期退出码 0
python -m unittest discover -s tests -v 全部治理测试通过,不证明阅读器、NLP、SRS 已实现
git status --short --branch 显示分支与待提交变化,提交后应无意外改动

同步会保护已有未提交镜像改动。新项目首次导出前先保存原有资料并提交引导材料;不跳过脏文件检查。凭据只通过安全配置或环境变量 GITEA_TOKEN 提供,不进入命令文本、日志或 Git。当前 MCP 连接其他站点,使用 API 的原因见初始化记录。

常用调试方式

先处理第一个可定位错误。页面缺失先发布、回读,不伪造 revision;配置错误先核对 owner/repository。产品阶段按用户/语言、任务、API、Worker/NLP 和数据文件逐层追踪,日志避免个人正文和凭据。

完成修改前

文档检查来源、链接、事实/建议区分和工作量加总;工具运行受影响的现有测试。产品阶段按具体工单测试隔离、Unicode、幂等、复习或恢复,未执行部分如实记录。当前没有 go.mod、package.json 或 Compose,不能声称产品能启动。

可复制 Shell 示例

PowerShell 语法与外部命令

  • Windows 环境默认使用当前 PowerShell 语法,不套用 Bash 的 heredoc、变量、路径、引号或反斜杠转义规则。只有明确调用 Bash、WSL 或 Git Bash,并确认路径与编码边界时,才使用 Bash 专用语法。
  • 不需要变量展开的正则和字符串优先用单引号;单引号字符串内部的单引号写成两个单引号。需要变量展开时才使用双引号。
  • 正则同时包含单双引号或转义复杂时,优先赋值给变量、使用 rg -e,或拆成语义等价的简单查询;不得为了避开转义改变 AND/OR 条件或重复执行无关搜索。
rg -n 'error|warning' docs
$pattern = 'can''t match "value"'
rg -n -e $pattern docs

PowerShell 不支持 Bash heredoc。需要把多行 Python 送入标准输入时,使用单引号 here-string,避免 PowerShell 展开 Python 中的 dollar sign($)等内容。起始标记后必须立即换行,结束标记必须单独占一行:

@'
print("hello")
'@ | python -

foreach、if 等语句块不能裸放在管道左侧;需要管道输出时使用 $()、@() 或先赋值。普通命令输出无需包装:

@(foreach ($number in 1..3) { $number }) |
    Measure-Object

不要把含 * 的搜索路径直接作为 rg 路径参数。优先传入真实目录,并用 -g/--glob 让 ripgrep 筛选路径:

rg -n -g '*.md' 'PowerShell' docs

只有确实需要把实体路径列表交给其他命令时,才使用 Get-ChildItem 展开并传递 .FullName;不要把 Get-ChildItem -Filter 设为所有 rg 搜索的固定前置。

示例预期:Python 输出 hello;Measure-Object 返回 Count 为 3;rg 返回当前文档中匹配 PowerShell 的行(有匹配时退出码 0,无匹配时为 1)。这些是 Shell 使用示例,不是产品测试命令。

学习端工程创建后的验证方向

学习端拟使用 create-vue + Vite,测试组合为 Vitest + Playwright;管理端保持指定 go-admin-ui 的现有工具链。当前未创建学习端工程,因此尚无可执行的产品 npm/pnpm 脚本。本轮技术方案记录不改变该事实。

正式工程建立时记录精确 Node/包管理器/依赖版本、锁文件、构建和测试命令;分别验证两端构建、共享登录/API 授权,以及阅读选择、Unicode 定位和学习闭环。具体测试范围依赖后续 MVP,不提前声称全部业务场景已有覆盖。

工程基础运行与验证(#2)

已验证主机为 Windows PowerShell;Node 22.22.1、pnpm 9.15.1、Go 1.26.5、MySQL 8.4.3。默认本地入口:学习端 http://127.0.0.1:5173、管理端 http://127.0.0.1:5174、API http://127.0.0.1:8000。前端 /api 由开发代理转发到后端,不开启宽泛跨域。

  1. 在 MySQL 8 中准备独立空库 lexgo_dev 与 lexgo_test_issue2(utf8mb4);不要把既有业务库用作测试库。
  2. 复制根 .env.example 为 .env.local,填写本机 DB 凭据与初始管理员密码;.env.local 已被 Git 忽略。优先读取进程中已有 LEXGO_* 配置,文件仅补缺项。
  3. 在仓库根执行以下后端命令。migrate 显式建表,bootstrap 仅首次执行,serve 不修改结构。
python scripts/server.py migrate
python scripts/server.py bootstrap
python scripts/server.py serve

分别开终端启动两个前端:

npx --yes pnpm@9.15.1 --dir admin install --frozen-lockfile
npx --yes pnpm@9.15.1 --dir admin dev
npx --yes pnpm@9.15.1 --dir learner install --frozen-lockfile
npx --yes pnpm@9.15.1 --dir learner dev

验证命令(仓库根执行):

python scripts/server.py test-integration
python scripts/server.py build
npx --yes pnpm@9.15.1 --dir admin test
npx --yes pnpm@9.15.1 --dir admin lint
npx --yes pnpm@9.15.1 --dir admin build
npx --yes pnpm@9.15.1 --dir learner test:unit --run
npx --yes pnpm@9.15.1 --dir learner test:e2e
npx --yes pnpm@9.15.1 --dir learner build

test-integration 默认使用 lexgo_test_issue2,可由 LEXGO_TEST_DB_NAME/LEXGO_TEST_DSN 指定其他 lexgo_test_ 前缀专用库;拒绝其他名字。迁移测试创建随机同前缀空库并仅清理自己创建的库,因此测试账号需具备该测试实例中的建库权限。普通 test 命令在没有测试库设置时跳过 MySQL 用例,不能当作集成通过。

学习端 Playwright 用例使用已安装的 Chrome 和虚构 API 响应;真实 MySQL/API 的浏览器联测结果在工单记录,不能混为同一种测试。管理端会话测试使用 Node 内置 runner。新增功能需针对行为写测试,而不是只验证打包。

本次本机初始管理员名为 admin,密码由工具随机生成,保存在忽略的 .env.local 的 LEXGO_BOOTSTRAP_PASSWORD;虚构浏览器学习账号 learner_a/learner_b 的本机测试口令记录在忽略的 .local/browser-accounts.json。这些本机文件不随 Git 交付,其他环境应自行设置。

停止开发服务器使用对应终端 Ctrl+C。回退本单代码可切换前一提交,保留项目库;前一版本只有文档,不存在需要启动的旧产品。删除测试/开发库不是自动回退步骤;生产升级与完整备份恢复由交付工单 #15 另行验证。

本机 supervisor 启动实例(2026-09-10)

用户要求将两端开发服务接入 D:/supervisord。配置文件为 D:/supervisord/supervisord.conf,控制台为 http://127.0.0.1:9009。使用现有 supervisor 启动入口;三个实例随 supervisor 启动,并在意外退出后自动重启。这不代表已配置 Windows 开机启动,也不替代 #15 的生产部署。

实例 工作目录 页面地址 日志
lexgo-learner D:/opc_project/lexgo/learner http://127.0.0.1:5173 D:/supervisord/lexgo-learner_out.log、lexgo-learner_err.log
lexgo-admin D:/opc_project/lexgo/admin http://127.0.0.1:5174 D:/supervisord/lexgo-admin_out.log、lexgo-admin_err.log

两端通过 C:/nodejs/node.exe 直接调用已经安装的 Vite / Vue CLI,启动时不安装依赖。学习端附带 --strictPort,避免端口冲突时自动换端口。管理端实例单独设置 npm_config_manage_package_manager_versions=false,防止 Vue CLI 编译后的 pnpm --version 检查触发全局 pnpm 自动下载指定版本而卡住;没有修改全局 pnpm 设置或项目锁文件。

通过控制台对 lexgo-learner / lexgo-admin / lexgo-api 单独 Start、Stop、Restart。启用托管时不要再执行同端口的手动 dev 命令。更改配置前备份原文件;本次备份为 D:/supervisord/supervisord.conf.lexgo-20260910-193432.bak。该备份只在本机保存,不提交仓库。

当前使用的 Go supervisord v0.6.8 重载可添加、移除实例,但现有实例对象不重新读取 command/environment。修改已有实例配置时,先停止目标实例,临时移除该节并重载,再加入新节并重载;不要重启整个 supervisor。回退本次接入时仅停止和移除上述两个实例,再重载,不覆盖随后新增的其他配置。

用户随后确认将共用后端一起托管。新增 lexgo-api,工作目录 D:/opc_project/lexgo/server,命令 D:/opc_project/lexgo/server/lexgo.exe serve,监听 127.0.0.1:8000。日志为 D:/supervisord/lexgo-api_out.log、lexgo-api_err.log。已启用 autostart、autorestart,启动不执行迁移或 bootstrap。

supervisor 直接管理编译后的 Go 进程,运行时不调用 Python。数据库连接值从本机 .env.local 复制到该实例的 environment,仅保存在本机 supervisor 配置中,不包含 bootstrap 密码。今后数据库连接配置变化,需要同步更新两处本机配置并按下述方式重新创建实例;不要打印配置或提交 Git。API 仍依赖本机 MySQL。

修改后端源码后,从控制台停止 lexgo-api,在仓库根目录执行 python scripts/server.py build,再启动 lexgo-api。Windows 中运行的 exe 不能直接覆盖;如果构建失败,修复后重新构建成功再启动。Python 此时只是构建辅助脚本。手动 python scripts/server.py serve 仍可用于临时调试,但必须先停止托管实例,避免 8000 端口冲突。

后端交接验证:停止时 8000 端口释放,重新启动后 lexgo-api 为 Running、/healthz 返回 200,两端 /api/v1/me 均返回预期 401。原有 11 个实例(含两个前端)的 PID 与状态均未变化。

验证结果:两个实例均为 Running,两端首页 HTTP 200,未登录请求 /api/v1/me 经代理返回预期 HTTP 401;已有 9 个实例的状态和 PID 均未变化。已交接并停止之前的手动前端进程。

本机验收账号维护(2026-09-10)

用户指定的管理账号 admin 已更新密码,并创建普通学习账号 dev 及其独立英语空间。指定密码只保存在忽略的 .local/account-credentials.json;.env.local 的 LEXGO_BOOTSTRAP_PASSWORD 已同步管理员当前密码,但仍不可对已有数据库重复执行 bootstrap。此前 learner_a / learner_b 测试账号保持原样。

两个指定账号通过本机管理员事务设置密码哈希并撤销旧会话;这是已获用户授权的本地维护操作。普通管理页面新增账号和重置密码均要求 6~72 字节,登录则直接验证已有非空密码,不以新密码下限拒绝已有账号。

新增真实 MySQL 回归测试覆盖已有短密码登录、错误密码拒绝、创建/常规重置仍拒绝短密码;已观察测试先失败再通过,完整后端集成测试通过。重新编译并由 supervisor 启动 lexgo-api 后,admin 管理端登录和 dev 学习端登录均成功,dev 的 /accounts 返回 403、英语空间归属正确,退出后的 token 返回 401。

新增账号 6 字节密码验证:管理端会话测试 18 项及完整后端 MySQL 集成测试通过;新增覆盖 5/6/72/73 字节和多字节字符,创建成功后验证登录。管理端 lint、生产构建及后端构建通过,lexgo-api 已由 supervisor 重新启动。

重置密码下限已同步为 6 字节。管理端测试与真实 MySQL 集成测试覆盖 5/6/72/73 字节及多字节字符,验证新密码登录成功、旧密码和旧会话被拒绝。管理员页面的新增/重置提示均为 6–72 字节;本次不修改现有账号密码。

审计模块启动与验证(#18 / schema v2)

首次更新此版本时先在 supervisor 停止 lexgo-api,再从仓库根目录执行 python scripts/server.py build、python scripts/server.py migrate,随后启动 lexgo-api。不要在 Windows 中覆盖正在运行的 exe。新服务要求 schema v2,启动本身不迁移;旧 v1 的账号、学习空间、会话完整保留。

本机已执行 v1→v2 并验证 sys_user 4 行、lexgo_spaces 4 行、lexgo_sessions 2 行迁移前后的整体指纹相同。原二进制仅在忽略目录 .local/lexgo-pre-audit.exe 留作本机回退,不提交 Git。后续正常启动仍通过 supervisor;登录日志入口 http://127.0.0.1:5174/#/login-logs,操作日志入口 http://127.0.0.1:5174/#/operation-logs。

手动清理过期审计记录:python scripts/server.py audit-cleanup;仅删除两张审计表中超过 90 天的记录,执行前确认本机 LEXGO_DB_NAME。后台自动清理在服务启动时及每小时运行,每次限制一分钟,失败只记录固定提示并于下一小时重试。

回退到已验收 #2 时:停止 API,确认没有其他 v2 实例,再恢复 .local/lexgo-pre-audit.exe;将 lexgo_schema 中 id=1、product=lexgo 的版本从 2 改回 1 后启动旧程序。两张日志表保留,不删除,旧程序不继续记录新日志;重新升级时显式 migrate 恢复 v2。本次已验证增量/重复迁移及业务数据保留,没有在开发库演练回退。

后端 MySQL 集成测试覆盖审计权限、查询边界、创建/启停/重置审计、无敏感输入、事务回滚、90 天边界、增量与重复迁移;管理端 24 项测试、lint 和生产构建通过。浏览器已展示真实登录与重复创建失败记录。为翻页检查生成了 21 条 fixture.audit 虚构账号登录失败记录,不创建该账号;记录按相同 90 天规则保留。浏览器自动化点击未触发页面事件,用户本次已确认日志模块验收通过;自动化点击检查仍保留原始未通过记录。

#18 用户验收:2026-09-10T20:40:04+08:00 用户确认日志通过验收(工单评论 7591),包含此前待人工检查的交互。未重新运行自动化测试,未更改其历史结果,未合并 PR 或发布生产。

#3 英语离线验证入口与复现

从仓库根目录执行(uv 与 Node 已安装,不能使用本机默认 Python 3.8):

uv venv --python 3.12.12 .local/nlp-venv
uv pip install --python .local/nlp-venv/Scripts/python.exe -r spikes/english/requirements.lock
.local/nlp-venv/Scripts/python.exe spikes/english/setup_resources.py
uv pip install --python .local/nlp-venv/Scripts/python.exe --no-deps .local/nlp-resources/en_core_web_sm-3.8.0-py3-none-any.whl
.local/nlp-venv/Scripts/python.exe -m unittest discover -s spikes/english -v
node --test spikes/english/view.test.mjs
.local/nlp-venv/Scripts/python.exe spikes/english/benchmark.py
.local/nlp-venv/Scripts/python.exe spikes/english/app.py

打开 http://127.0.0.1:5183/,默认虚构样例,分析后点击 went 应出现 go 与“按原形查询”;dog 直接命中,zzzxqvfiction 未找到。--resources .local/absent-resources 可验证词典缺失,--port 可更换临时端口。模型缺失、词典缺失、非法输入及内部错误不输出路径/正文。仅 loopback,Host/Origin 校验,禁跨域、无缓存、无访问日志、连接读超时 10 秒。未配置 supervisor;停止该临时进程即可回退,既有服务和数据不变。

首次准备需要联网,失败可重跑;资源文件通过固定 SHA256 校验后使用。模型 3.8.0 MIT,WordNet 3.0 ZIP 完整保留 LICENSE/版权/免责声明,spaCy MIT、NLTK Apache-2.0。固定资源 URL 和摘要见 spikes/english/resources.json,原始许可与来源见该目录 README。词典为英英格式,不是中文翻译库。

2026-09-10 实测:11 项 Python 测试和 2 项 JavaScript 测试通过。真实模型/词典测试及 benchmark 禁止 socket connect,验证运行期无在线翻译依赖;不是整机断网测试。浏览器已实测展示分词、点击 went→go、手动查询无结果。外部 spaCy/Click 有一条 DeprecationWarning,未影响测试结果。Windows 10 19044,Intel Family 6 Model 140、8 逻辑核,Python 3.12.12;完整环境、UTC 时间和样本保存在 benchmark-result.json。

测量 本次样本
冷进程 Engine 加载(含 import,文件系统缓存可能已热) 2355 ms
首次分析 / 首次 dog 查询 见 benchmark-result.json(各 1 次)
100000 code point(106095 UTF-8 字节),spaCy 3 次 中位 1433 ms,约 6.98 万 cp/s
同文正则+WordNet morphology,3 次 中位约 336 ms;未包含三位置转换,非完全等价负载
dog 查询,热进程 100 次 中位 0.0149 ms,p95 0.023 ms
12 个显式 lemma 样例 spaCy 11/12、基线 6/12;保留 leaves 错误

推荐 Python NLP,但该样本不代表一般准确率、生产并发能力或延迟保证。尚未验证正式 Go/Python 调用、长任务持久化、移动端划词(#4)、英汉词典及生产部署。

#3 于 2026-09-10T21:50:14+08:00 通过用户验收(工单评论 7609),独立小样验证完成。此前实测结果和限制保持;本次验收未重跑测试。

#4 阅读选择验证

启动:python spikes/selection/serve.py,本机 http://127.0.0.1:5184/。默认仅 loopback,提供白名单静态文件,无目录列表或写入接口。虚构英文两章,任意词/连续短语可选;模拟释义与保存仅页面内有效。完整操作及真机清单见 spikes/selection/README.md;本单不配置 supervisor。

自动测试:node --test spikes/selection/range.test.mjs spikes/selection/ui.test.mjs。Node 22.22.1;范围测试无外部依赖,DOM 测试复用 learner 锁定的 jsdom(需要时先 pnpm --dir learner install --frozen-lockfile)。11 项通过,覆盖原文分区、反选、词边界、跨行/标点、组合字符/emoji/ZWJ、三位置单位、重复/重叠、关闭后键盘续读、标点取消旧选区、DOM Range 映射与显式保存。关闭丢失键盘位置及标点残留均复现旧代码失败,再验证修正通过。JS 语法检查通过。

桌面 Chrome 实际验证:鼠标拖选 Mira opened the workshop,原生选择与正文高亮相等;按钮扩展终点后可包含 before;键盘连续选择及关闭后下一词续读正常;模拟保存与词典不可用提示正常。章节中段直接点击关闭前后 scrollY 同为 909.5999755859375。自动化 locator.click 会在事件之前滚动到按钮,因此用基于可见坐标的直接点击复核;页面“验证设置”记录事件内关闭前后位置,避免将工具预滚动误判为页面变化。

桌面窄屏约 390×844 CSS 像素检查无横向溢出,底部面板正常;键盘选中词底部 464.4、面板顶部 489.5,未被遮挡。此为桌面浏览器宽度检查,不是移动系统或触摸模拟结果。视口已恢复。

真机项未通过:尚未取得实际手机浏览器结果。 需要用户记录设备/系统/浏览器版本、长按/手柄扩缩跨行范围、普通滚动、底部面板遮挡、虚拟键盘以及关闭位置,回填 #4。可在明确进行测试时以 --host 指定电脑实际局域网 IPv4,手机同网访问;不自动放行防火墙或开放公网。用户已明确验收 #4 并关闭工单;该详细测试证据缺口仍保留,不能将其改记为已执行通过。

独立规格及代码评审已修复已知问题;没有真实账号、生产数据、词典联网或 SRS 测试。停止小样即可回退,现有学习端/管理端/数据库不变。#21 附件不在本次范围。

#4 用户验收记录:2026-09-10T22:51:52+08:00 用户确认“#4通过验收”(评论 7636)。未补充手机型号/浏览器/操作记录,未重跑测试,未自动合并 PR。后续正式移动端集成应补真机回归。

#5 粘贴导入与章节阅读(schema v3)

升级步骤(本机,仓库根执行):停止 lexgo-api → python scripts/server.py build → python scripts/server.py migrate → 启动 lexgo-api。lexgo_dev 已从 v2 升到 v3,新增 lexgo_books、lexgo_chapters、lexgo_ingest_jobs;迁移前后 sys_user 4、lexgo_spaces 4、lexgo_sessions 3、lexgo_login_logs 23、lexgo_operation_logs 1 完全一致。托管实例重启后 /healthz 返回 200,两端首页仍为 200,lexgo-admin 与 lexgo-learner 的 PID 未变化。

回退:停止 API,把 lexgo_schema 中 id=1 的版本从 3 改回 2,并恢复上一二进制;三张新表保留不删除,旧程序不读写它们。重新升级时显式 migrate 重新执行 IF NOT EXISTS 语句即可;集成测试覆盖 v2→v3 的既有数据保留与 v2 标记下的重复迁移。未在开发库演练回退。

学习端入口:http://127.0.0.1:5173 → 登录 → 我的书库 → 粘贴文本导入 → 章节就绪后进入阅读。

测试命令与结果(仓库根执行;本单使用专用库 lexgo_test_issue5,不借用其他测试库):

命令 本次结果
python scripts/server.py test-integration(LEXGO_TEST_DB_NAME=lexgo_test_issue5) 全部通过:18 个顶层用例,其中 #5 新增 8 个(7 个书库/章节/任务/阅读 + 1 个 v2→v3 数据保留),另含 12 个子用例;既有 10 个用例保持通过
npx --yes pnpm@9.15.1 --dir learner test:unit --run 3 个文件 31 项通过(session 9、library 16、reading 6)
npx --yes pnpm@9.15.1 --dir learner build vue-tsc 类型检查与 vite 构建通过,退出码 0
npx --yes pnpm@9.15.1 --dir learner test:e2e 3 项通过(既有 auth 2 项 + 新增 reading 1 项,均为虚构 API 响应)

真实 API + MySQL 实测(2026-09-10,lexgo_dev)

使用本单新建的虚构账号 issue5_a、issue5_b,口令只保存在忽略的 .local/issue5-accounts.json;未改动 admin、dev、learner_a、learner_b。脚本 .local/verify-issue5-api.ps1 只在本机运行,不输出口令。

  • 粘贴:HTTP 201,章节与任务均为 pending,charCount 99。
  • 处理:实测状态序列 pending → ready,约 1132 ms(后台任务每秒轮询);job attempts=1。
  • 阅读:originalText 与提交正文逐字符相等,CRLF、制表符、弯引号、破折号、省略号、é 加组合重音、emoji、行尾空格与空行全部保留;sha256 前缀 ce7357ea22a3。
  • 幂等:同一 requestId 重复提交 HTTP 200、duplicate=true、章节与任务编号不变;同一 requestId 换正文 HTTP 409;书库仍为 1 本。
  • 隔离:issue5_b 读取 issue5_a 的书籍、章节、任务以及追加、重试全部 404;请求体带 ownerId 与查询参数 ownerId 均 400;issue5_b 书库为空。
  • 追加与阅读导航:新章节 ordinal=2,处理后就绪,前后章节编号互相指向。
  • 校验:空标题、纯空白正文、非 en 语言、缺少 requestId、超过 100000 code point 分别返回 400 与可读中文提示。
  • 遗留 fixture:lexgo_dev 中 issue5_a 名下 1 本虚构书、2 个就绪章节(bookId=1,章节 1、2)。

浏览器实测:真实学习端 + 真实 API + 真实 MySQL 联测(临时 Playwright 用例,运行后删除):issue5_a 登录 → 书库显示既有虚构书与“导入内容”入口 → 导入页粘贴含空行、制表符、行尾空格与 emoji 的正文 → 书库页由“处理中”变为“已就绪” → 阅读页 article.reader-text 的 textContent 与粘贴正文逐字符相等、computed white-space 为 pre-wrap → “下一章”切换到第二章且正文精确相等 → 390×844 视口下横向溢出 0 px。同一轮还运行了 3 项虚构 API 的既有 e2e,共 4 项通过。

截图保存在本机 .local/evidence/(library.png、book-after-paste.png、reader-desktop.png、reader-mobile-390.png)并作为工单附件上传,便于人工目视复核;本次会话模型不能读取图片,截图未经 Agent 目视检查,功能断言来自上面的程序化检查。

未验证:处理失败到重试的用户界面路径只由集成测试覆盖(无法通过 API 主动制造处理失败);真实手机浏览器长按、手柄、滚动与虚拟键盘仍属 #4 缺口,本次只用桌面浏览器窄屏检查,不能当作真机结果;Python NLP 未接入,token、lemma 与词典仍为 #3 小样范围;生产并发、容量、备份恢复与部署不在本单范围。

#5 审核整改验证(R1~R4,2026-09-11)

整改提交与完整证据见工单 #5 的整改评论。本次复核命令与结果(仓库根执行,专用测试库 lexgo_test_issue5):

命令 本次结果
python scripts/server.py test-integration 20 个顶层用例全部通过(新增 TestMySQLIngestRecoveryWithoutRestart、TestMySQLIngestAttemptsAreBoundedAndManualRetryRestarts)
npx --yes pnpm@9.15.1 --dir learner test:unit --run 38 项通过(library 21、reading 8、session 9)
npx --yes pnpm@9.15.1 --dir learner build 通过(vue-tsc + vite)
npx --yes pnpm@9.15.1 --dir learner test:e2e 3 项通过(虚构 API 响应)

回归测试的有效性:新增的前端 7 项用例先在整改前的 library.ts/ImportView.vue 上运行并实际失败(追加发送 language、离页后响应写回、重试后停在失败),改回修复版本后全部通过。

真实联调(lexgo_dev,虚构账号 issue5_a):

  • R1 追加:真实学习端从书籍页进入“追加章节”,提交后回到书籍页,新章节就绪后可阅读;抓取到的请求体只有 requestId、title、text,无 language;正文逐字符相等。
  • R2 恢复:新建章节后用 SQL 把任务与章节置为 processing 且 updated_at 早于阈值(UTC 时间),不重启服务,运行期清扫在 544 ms 内把任务重新入队并发布为就绪,章节与任务编号不变,正文逐字符相等。
  • R4 重试:SQL 制造真实失败任务(content_changed)后,在浏览器点击“重试”并中断其后的第一次刷新请求,页面立即由“处理失败”变为“处理中”,随后自行变为“已就绪”,无需手工刷新。

注意:MySQL 会话时区为 SYSTEM(本机为 UTC+8),而服务按 UTC 存储 DATETIME;核对任务时间时使用 UTC_TIMESTAMP 而不是 NOW(),否则会出现 8 小时的假偏差。

未在本轮验证:R3 的真实浏览器时序(离页与响应同时发生)只由单测覆盖;真机手机证据仍属 #4 缺口。

#6 部署与验证(2026-09-11)

正式后端 schema v4,只增加 lexgo_dictionaries。显式 migrate 后启动纯Go lexgo.exe;Python scripts/server.py 仍是开发命令封装,产品运行不依赖Python NLP。

本机已从v3升级v4并重启lexgo-api,升级前后sys_user/lexgo_spaces/lexgo_sessions/lexgo_books/lexgo_chapters/lexgo_ingest_jobs计数一致。旧二进制保存在忽略的 .local/lexgo-pre-issue6.exe。回退:停止API,恢复旧二进制,将已确认v4的schema标记恢复3,保留新增资源表及全部学习数据,再启动旧API;不要删除数据或重新bootstrap。

管理端 http://127.0.0.1:5174 的“英语词典”页可下载指定包并导入/启停;本机已导入固定WordNet3.0,155287个词形/词性索引项。学习端 http://127.0.0.1:5173 打开本人ready章节,点词或Enter/空格查询,Escape关闭。went/mice应出现go/mouse候选。源包、本机凭据与测试证据仅存在忽略的.local,不进入Git。

验证:Go全包MySQL集成(专用lexgo_test_issue5)与go vet通过;学习端53单测、类型检查/构建、默认5173 Playwright3项通过;管理端31单测与lint通过,构建含已有Sass弃用与bundle体积提示;治理56测试与strict通过。实际ZIP解析、schema3→4原文/任务保留、权限、失败保留、重复启用、冷缓存路由重建都有覆盖;冷缓存测试不是完整备份恢复演练。

主审真实API联调使用真实管理端session模块经5174代理上传;经5173代理两测试账号分别创建虚构章节并验证精确/不规则词形、Unicode原文片段、越权404/普通用户管理403、停用/重复导入启用、错误ZIP保留资源。新增测试书籍id3/4、章节id8/9归issue5_a/issue5_b,没有修改其他账号的书籍。

常驻5173一度返回空白页:Vue模块转换500、代理缺失;同代码隔离服务正常,只重启lexgo-learner加载配置后恢复,默认E2E通过,未改启动配置。桌面交互浏览器工具因旧会话失效未完成手工真实UI联调;已有项目Playwright使用模拟API,真实API验证另列。手机仅窄屏自动测试,真机证据仍未补齐。

#7 验证与迁移(2026-09-11)

仓库根执行;Go 工具链由 python scripts/server.py 固定 go1.26.5。本单使用专用测试库 lexgo_test_issue7,不借用其他测试库。

命令 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue7 python scripts/server.py test-integration 34 个顶层用例全部通过、0 跳过;含 #7 新增 3 个 MySQL 用例、4 个单元用例和 1 个 v4→v5 迁移用例
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 58 项单测、类型检查、构建、3 项默认 E2E 全部通过
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过

覆盖内容:非法状态与等级边界、文本上限、词形身份与大小写合并、词形不按 lemma 合并、重复与并发保存只留一条记录、跨章节同形词状态一致、两账号互不影响、篡改 owner/language/term 被拒绝、非本人章节 404、未登录 401、未就绪章节 409、v4→v5 迁移保留既有数据与检查约束。

本机开发库 lexgo_dev 已显式从 v4 升级到 v5:升级前后 sys_user 6、lexgo_spaces 6、lexgo_sessions 8、lexgo_books 4、lexgo_chapters 9、lexgo_ingest_jobs 9、lexgo_dictionaries 1 全部不变,新增空的 lexgo_terms。旧二进制备份在忽略的 .local/lexgo-pre-issue7.exe。回退:停止 lexgo-api,恢复旧二进制,把 lexgo_schema 标记改回 4 后启动;保留 lexgo_terms 与全部既有数据,不删除数据、不重新 bootstrap。

真实链路验证:真实 Go API+真实 MySQL 共 30 项检查通过(凭据只从本机安全配置读入进程),覆盖两个虚构测试账号 issue5_a/issue5_b 的登录、保存、幂等、状态边界、跨章节一致、跨账号隔离与越权拒绝;随后用临时 Playwright 用例在真实学习端+真实 API 上以两个账号复核保存、重新加载后的高亮与预填,以及 390×844 窄屏底部面板。截图保存在本机 .local/evidence/(issue7-reader-desktop.png、issue7-reader-mobile-390.png、issue7-account-b.png),临时用例运行后删除。

未验证:真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器窄屏检查,不当作真机结果。

#8 验证与迁移(2026-09-11)

仓库根执行;Go 工具链由 python scripts/server.py 固定 go1.26.5。本单使用专用测试库 lexgo_test_issue8,不借用其他测试库。

命令 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue8 python scripts/server.py test-integration 41 个顶层用例全部通过、0 跳过;含 #8 新增 6 个复习用例与 1 个 v5→v6 迁移用例
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 71 项单测、类型检查、构建、5 项 E2E 全部通过
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过

覆盖内容:间隔表与每个状态转换(含等级上下限)、到期边界(due_at = now 到期、早 1 毫秒不到期)、入队范围(已知/忽略不入队)、两账号队列互不可见、重复提交只记一次、同一词条双标签页 second 写入为 stale、作答归属 404、已知词条 409、未知评分/缺少到期时间/未知字段 400、显式等级的排期规则、幂等键唯一约束、v5→v6 保留既有词条与计数。

本机开发库 lexgo_dev 已显式从 v5 升级到 v6:升级前后 sys_user 6、lexgo_spaces 6、lexgo_sessions 5、lexgo_books 5、lexgo_chapters 10、lexgo_ingest_jobs 10、lexgo_dictionaries 1、lexgo_terms 4 全部不变;新增 lexgo_term_reviews 4 行(全部 due_at = created_at)与空的 lexgo_review_answers。旧二进制备份在忽略的 .local/lexgo-pre-issue8.exe。回退:停止 lexgo-api,恢复旧二进制,把 lexgo_schema 标记改回 5 后启动;新表不影响旧二进制写入个人词条,保留新表与全部既有数据,不删除数据、不重新 bootstrap。

真实链路验证:真实 Go API+真实 MySQL 共 38 项检查通过(凭据只从本机安全配置读入进程,脚本可重复运行),覆盖到期即时入队、答对升级与 1 天排期、答错立即回队、重放与 stale 不重复推进、越权与非法输入拒绝、两账号互不影响、已知词条拒绝作答;随后用临时 Playwright 用例在真实学习端+真实 API 上完成「登录→粘贴导入→点词查义→保存个人释义→到期复习作答→回到阅读器确认状态」的完整闭环,并复核 390×844 窄屏无横向溢出。截图保存在本机 .local/evidence/(issue8-reader-saved.png、issue8-review-revealed.png、issue8-review-summary.png、issue8-reader-reviewed.png、issue8-review-mobile-390.png),临时用例运行后删除。

运维记录:本次只有 lexgo-api(新二进制)与 lexgo-learner 被重启;学习端常驻 Vite 在长时间运行后一度对 /src/style.css 返回空样式表,导致 E2E 观察到 white-space: normal,重启后恢复,未改动代码或配置。其余实例保持 Running。

未验证:真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器窄屏检查,不当作真机结果。并发只覆盖到「双标签页同一词条」这一层,没有做多用户压测。

#8 审核整改(R1~R3,2026-09-11)

独立审核(Claude Code)只读审阅提交 0328505/ec5ec2d,指出三处影响验收标准第 2、3 条的问题。三点均先在 lexgo_test_issue8 写复现用例观察到失败,再修复并转绿。

问题 现象与根因 修复 回归用例
R2 并发同键返回 500 两个同时到达、带同一 answerId 的请求都在加锁前查不到记录;后者进入 stale 分支插入相同 answer_key,触发唯一键冲突返回 500 取得词条行锁后再用加锁读复查一次答案键,命中直接返回首次结果;stale 插入遇到 1062 也转为返回记录 TestMySQLReviewConcurrentReplayOfOneAnswer(两个 goroutine 同键提交,两个都 2xx、review_count = 1、只有一条答案记录)
R3 编辑文本会重排复习 saveTerm 无条件调用 syncTermReview,编辑释义/例句也会把 due_at 重算,逾期词条被挤出当天队列 保存前加锁读取旧行,只有新建或状态/等级实际变化才移动 due_at;缺行时补建排期行 TestMySQLReviewEditKeepsSchedule(逾期 3 级词只改释义:due_at 与队列不变;改等级则重排)
R3 附带发现:面板保存把等级重置为 1 阅读器面板只提交状态不提交等级,termLevel 对缺省等级一律返回 1,于是 4 级词改一个错字会掉到 1 级 保存未提及等级时保留已获得的等级;只有进入 学习中 才从 1 开始 TestMySQLReviewPanelSaveKeepsLevel(4 级词面板式保存后仍为 4 级,且排期不变;退出再进入学习中则从 1 开始)

答案契约随之明确:作答响应 result 只取 applied/stale,另加 duplicate 布尔标记。重放返回首次结果并把 duplicate 置真,客户端因此可以按首次结果计数:网络把响应丢掉后点「重试提交」拿到 duplicate=true 的 applied,本轮计数正常增加,完成页不会退化成「今天没有到期词条」。已应用的作答返回 201,重放与 stale 返回 200(R1)。

提示与注释(R4、R5):卡片因 stale 离开时页面显示 role="status" 提示「该词已在其他页面复习,本次未计分。」,重放且首次为 stale 时显示「该词已按上一次的评分记录,未重复计分。」;本轮只解决卡片而没有新计分时,完成页显示「本轮没有新的计分:N 个词条已在其他页面复习。」。answerId 的作用域注释改为「每张卡片一个,失败重试复用」,与 answerIdFor 的实现一致。

流程记录(R6):评论 7769 的方案写的是在 lexgo_terms 上增加列,实际实现改为独立表 lexgo_term_reviews(加法迁移可重试、不对既有表做 ALTER)。该变更在实施评论 7776 与 Wiki 中说明了原因,但没有按「数据结构变化先更新工单」的要求在实施前追加变更评论;本页与上文契约按实际实现记录,方案评论中的「新增列均有默认值」以独立表为准。

整改后重跑:Go 单元与集成测试(专用库 lexgo_test_issue8)44 个顶层用例全部通过、0 跳过;学习端 73 项单测、类型检查、构建与 5 项 E2E 通过;管理端 31 项与 lint 通过;治理 56 项与严格检查通过;真实 API+MySQL 42 项检查通过(新增 4 项针对 R3 与重放契约);真实浏览器复核面板保存与复习闭环通过。截图 .local/evidence/issue8-fixed-summary.png。

#9 验证与迁移(2026-09-11)

仓库根执行;Go 工具链由 python scripts/server.py 固定 go1.26.5。本单使用专用测试库 lexgo_test_issue9,不借用其他测试库。本单不改动数据库结构,所以没有迁移步骤,回退只需换回旧二进制。

命令 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration 50 个顶层用例全部通过、0 跳过;含 #9 新增 6 个上传用例
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 80 项单测、类型检查、构建、6 项 E2E 全部通过
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过

覆盖内容:有效 UTF-8(含 CRLF、制表符、弯引号、em dash、省略号、emoji、组合字符)字节级往返、UTF-8 BOM 剥离且不进原文、只有 BOM、非法 UTF-8、Latin-1、UTF-16 大小端、NUL 字节、空文件、只有空白、超限与恰好边界(2 MiB、100000 码点)、缺 file、缺标题、缺或错误 language、缺或过短 requestId、未知字段、追加路径携带 language、非 multipart 请求、未登录、恶意文件名不影响存储、重复上传只产生一章、同编号换内容 409、追加他人书籍 404、两账号隔离、上传与粘贴共用同一任务管线。

真实链路验证:真实 Go API+真实 MySQL 共 26 项检查通过(凭据只从本机安全配置读入进程),覆盖有效文件与阅读器原文逐字节一致、BOM 不进入原文、五类无效文件、越权与未登录拒绝、两账号隔离、恶意文件名不泄漏;随后用临时 Playwright 用例在真实学习端完成「登录→切换 TXT→选择真实 UTF-8 文件→上传→处理中就绪→阅读器原文逐字符一致」的闭环,并复核 UTF-16 文件在浏览器预检阶段被拒。截图保存在本机 .local/evidence/(issue9-invalid-encoding.png、issue9-upload-processing.png、issue9-reader.png),临时用例运行后删除。

未验证:真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器检查。大文件并发上传只按单槽并发门设计,没有做多用户压力测试。UTF-16/GB18030 转码与按空行自动分章不在本单。

#10 验证与迁移(2026-09-11)

仓库根执行;Go 工具链由 python scripts/server.py 固定 go1.26.5。本单使用专用测试库 lexgo_test_issue9(沿用上一单的库),不借用其他测试库。本单不改动数据库结构,没有迁移步骤,回退只需换回旧二进制。

命令 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration 59 个顶层用例全部通过、0 跳过;含 #10 新增 9 个用例
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 94 项单测、类型检查、构建、7 项 E2E 全部通过
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过

覆盖内容:改名不改状态也不新建任务、标题/正文校验、正文变化产生新版本且保留章节编号、处理中改正文后旧任务只标 superseded 且不触碰新版本、旧版本任务重试 409、重复保存与改回原内容不新建任务、内容与版本不一致时按 content_changed 失败、恢复扫描作废过期版本且不重排新版本、编辑用原文对任意状态可读、删除章节后序号连续且导航正确、删除书籍级联清理章节与任务、删除期间在途任务不复活内容、并发删除同一章一个成功一个 404、重复删除 404、跨用户改名/编辑/删除/读原文一律 404、未登录 401、个人词条与复习排期在删除后完全保留。

真实链路验证:真实 Go API+真实 MySQL 共 36 项检查通过(凭据只从本机安全配置读入进程,脚本可重复运行并自行清理 fixture),覆盖改名、编辑产生新版本并重新处理、重复保存幂等、删除章节重排序号、删除书籍级联、个人词条与复习队列在删除后保留、越权与非法输入拒绝;随后用临时 Playwright 用例在真实学习端完成「导入→改名→编辑正文→新版就绪→删除章节→删除书籍→书库提示」的闭环,并核对阅读器原文等于新版本。截图保存在本机 .local/evidence/(issue10-book-after-edit.png、issue10-chapter-deleted.png、issue10-book-deleted.png),临时用例运行后删除。

未验证:真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器检查。并发只覆盖「同一章节并发删除」与「处理中编辑」两类,没有做多用户压力测试。浏览器 textarea 会把该章的 CRLF 归一为 LF,属已知边界,已记入业务规则页。

#11 验证与迁移(2026-09-11)

仓库根执行;Go 工具链由 python scripts/server.py 固定 go1.26.5。本单使用专用测试库 lexgo_test_issue9,不借用其他测试库。本单不新增数据库列或表,schema 保持 v6,没有迁移步骤,回退只需换回旧二进制。

命令 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration 64 个顶层用例全部通过、0 跳过;含 #11 新增 5 个短语用例
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 106 项单测、类型检查、构建、11 项 E2E 全部通过(含 #11 新增 12 单测与 4 项 E2E)
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过

覆盖内容:身份键的标点/换行/大小写/弯撇号归一、词数上限(12 与 13)、身份键与片段长度上限、切进单词被拒绝(服务端)、单词语义不被当成短语、跨章节匹配(同一短语在不同章节写法不同仍是同一条记录,两章都高亮,第一次出现两次)、最左最长与输入顺序无关、短语覆盖内部单词而不改动单词记录、短语进同一到期队列并作答(201 应用、重复提交回放)、越权 404 与未登录 401、编辑正文后不再出现则不报错且词条与排期保留、删除章节后词条保留;前端纯函数层覆盖整词对齐、内部保留、端点按词调整不反向、命中优先级与区间换算;E2E 覆盖真实鼠标拖选(Chromium 真实输入)→ 短语面板 → 保存 → 两次出现同时高亮 → 端点调整 → 点击已保存短语 → 复习整段挖空。

真实链路验证:真实 Go API+真实 MySQL 共 30 项检查通过(凭据只从本机安全配置读入进程,脚本自建两章 fixture 并在结束前删除书籍)。覆盖保存与身份键、显示原文保留标点、跨章节同一条记录与两处高亮、span 两端必须是词、短语内的单词仍是独立词条、单词/切词/伪造身份/越权/未登录的拒绝、到期队列与作答(含重复提交回放)、编辑正文后不再高亮但条目与排期保留、删除章节后条目保留。随后用临时 Playwright 用例在真实学习端完成「导入 → 选择范围 → 保存短语 → 两处高亮 → 点击已保存短语 → 复习整段挖空 → 清理 fixture」闭环,并用程序化选区在同一真实页面上验证范围映射与服务端身份一致。

未验证与已知限制:真实手机手柄与滚动的手感仍是 #4 起的既有缺口,本单只用桌面浏览器检查。Playwright 的合成鼠标拖拽在真实页面上不会扩展原生选区(在同一浏览器里对 mock 页面是成功的,程序化选区在真实页面也能唤起面板),因此真实链路的范围构建改用真实点击 + Shift 点击,连续拖选由 mock E2E 与程序化选区覆盖;真人鼠标拖选与真机手柄仍需人工复核。短语只在同一学习者与语言内匹配,不跨账号共享。

#12 验证与迁移(2026-09-11)

仓库根执行;Go 工具链由 python scripts/server.py 固定 go1.26.5。本单使用专用测试库 lexgo_test_issue9,不借用其他测试库。本单不新增数据库列或表,schema 保持 v6,没有迁移步骤,回退只需换回旧二进制。

命令 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration 69 个顶层用例全部通过、0 跳过;含 #12 新增 4 个用例
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 120 项单测、类型检查、构建、13 项 E2E 全部通过(新增 13 单测与 2 项 E2E)
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过

覆盖内容:查询解析与边界(未知键、重复键、page=0、limit=101、非法状态与类型、查询超长)、LIKE 通配符转义、空查询与去空白、三处命中(身份键/显示原文/中文释义)、大小写不敏感、搜索与状态/类型筛选的组合、分页(首/末/越界页与 total 稳定、排序稳定)、只返回本人与当前语言、编辑(只改文本不动排期、改状态重排、进入学习中按等级排期、已知或忽略立刻离开到期队列、改回新词立即到期、计数与作答记录保留、同内容重复提交无副作用)、等级与状态边界、身份不可编辑、空编辑与未知字段 400、越权 404 与未登录 401;前端覆盖列表与分页、搜索与筛选(含 URL 恢复)、空态与无结果清除、编辑对话框的等级选择器、保存失败保留输入、旧响应丢弃。

真实链路验证:真实 Go API+真实 MySQL 共 40 项检查通过(凭据只从本机安全配置读入进程,脚本可重复运行并自行恢复 fixture),覆盖列表字段与排序、大小写不敏感搜索、中文释义搜索、通配符字面值、类型与状态筛选组合、分页与越界页、参数拒绝、两账号隔离、未登录、编辑的排期规则与身份不可编辑。随后用临时 Playwright 用例在真实学习端完成「进入生词本 → 按类型筛选 → 搜索 → 编辑释义 → 保存 → 刷新后筛选仍保留 → 还原释义」闭环。截图保存在本机 .local/evidence/(issue12-vocab-list.png、issue12-vocab-none.png、issue12-vocab-edit.png、issue12-real-list.png、issue12-real-search.png、issue12-real-edit.png),临时用例运行后删除。

未验证:真实手机触屏与滚动证据仍属 #4/#11 起的既有缺口(由 #14 承接);本单只用桌面浏览器检查。搜索只做子串匹配,未做分词或相关度排序;大词汇量的性能未做专门压测(单页上限 100,列表查询使用 owner_id + language 前缀的既有索引)。

#13 迁移与验证(2026-09-15)

本单新增 schema v7(lexgo_chapter_progress),需要显式迁移:python scripts/server.py migrate。迁移只创建一张新表,不修改既有表与数据;回退时把 lexgo_schema.version 写回 6 并丢弃该表即可,业务数据不受影响(本机回退二进制保存在忽略的 .local/lexgo-pre-issue13.exe)。

开发库 lexgo_dev 迁移前后逐表计数一致(sys_user 6、lexgo_books 27、lexgo_chapters 33、lexgo_ingest_jobs 42、lexgo_terms 12、lexgo_term_reviews 12、lexgo_review_answers 55、lexgo_dictionaries 1、lexgo_login_logs 137、lexgo_operation_logs 5、lexgo_sessions 28),schema 6→7,快照见 .local/issue13-before.json 与 .local/issue13-after.json。

命令 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 70 个顶层用例全部通过、0 跳过(基线 67;本单新增 3 个用例)
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 130 项单测、类型检查、构建、15 项 E2E 通过(本单新增 10 单测与 2 项 E2E)
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过

覆盖内容:完成章节的幂等(第二次不移动时间、只有一行)、非 ready 章节 409、他人与不存在 404、未登录 401、阅读与阅读器/书籍列表的已读状态一致、正文新版本后回到未读(记录行保留、更新时间)、只改标题保持已读、章节删除后计数与记录同时消失、已知/忽略/学习中/新词分别计数、已保存词条总数、dueNow 与复习队列 total 一致、每本书的已读进度、另一账号与另一语言不串数据、迁移 v6→v7 与「回滚标记后重新升级」、新表一章一行的唯一性与级联删除。

真实链路验证:真实 Go API 与真实 MySQL 共 37 项检查通过(.local/verify-issue13-api.py,可重复运行,自建 fixture 书籍并在结尾删除),其中页面数字与 SQL 直查逐项比对(已达到/总可读章节、待复习、四种词条状态、已保存总数)全部相等;随后用临时 Playwright 用例在真实学习端完成「粘贴章节 → 处理完成 → 标记已读 → 书籍列表显示已读 → 进度页数字 → 刷新后一致 → 再次标记不重复计数」,并用 .local/tmp/check-issue13-page.py 把浏览器上显示的数字与数据库逐项比对(全部一致)后清理 fixture。截图保存在本机 .local/evidence/(issue13-reader-unread/read、issue13-progress 为 mock 流程;issue13-real-* 为真实链路),临时用例运行后删除。

本单顺带记录一个既有缺陷:编辑章节正文回到曾经用过的版本会返回 500(#32,lexgo_ingest_jobs 唯一请求键与 edit:<章节>:<内容摘要> 键冲突),不属于本单范围,未修改代码;真实验证脚本因此改为自建 fixture 而不改写既有正文。

未验证:真实手机触屏与滚动证据仍属既有缺口(#14 承接);阅读进度没有按书排序或搜索(进度页按书籍编号列出);没有大数据量下的统计性能压测(统计为带前缀索引的计数查询)。

#14 移动与主题验证(2026-09-15)

本单无 schema 变化、无迁移,纯学习端与文档改动;回退只需回到上一提交。

命令 结果
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 147 项单测、类型检查、构建、23 项 E2E(桌面 19 + 移动 4)全部通过;本单新增 20 单测与 8 项 E2E
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过;管理端无改动
python -m unittest discover -s tests / python dev_scripts/harness.py check --strict 56 项与严格检查通过
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 70 项通过(后端未改动,仍复跑一遍确认)

移动项目:npx playwright test --project=mobile 使用 390×844、isMobile 与 hasTouch 的 Chromium。覆盖内容:八个界面(书库/导入/书籍/阅读/查词面板/生词本/复习/进度)在 390px 下 scrollWidth ≤ innerWidth 且没有越界元素、每页都能返回书库、显示控件可用手指点开且高度不小于 36px、经 CDP 触摸事件的上滑真的滚动了章节且没有误开释义面板、面板不越过视口且页面为它预留底部空间、关闭面板后阅读位置不跳、深色与特大字号在刷新后保持、退出登录回到默认且同一账号回来仍是自己的选择。

主题回归测试:e2e/theme.spec.ts 逐个读回 16 个调色板变量,断言浅色与深色下都解析成非空值、页面背景不是透明、正文与背景不同色、深浅两套取值不同、Element Plus 的 --el-bg-color 也跟随。这条用例来自本单的一个真实教训:批量把颜色字面量替换为变量时,调色板自身的定义也被替换成了自引用变量,所有颜色静默失效——几何断言全过,只有焦点环探针把问题暴露出来,所以补了这条变量级回归。

真实链路:真实学习端+真实 Go API+真实 MySQL 走了一遍——粘贴 40 行长章节并处理完成,滚到 45% 后离开再返回,位置被恢复(偏差 < 150px,页面提示「已回到上次阅读位置」);切到深色+特大后刷新仍保持,正文实际字号变大(> 18px);在真实到期卡上按空格显示了答案(故意不评分,避免改动 fixture 账号的真实复习计划);另用 390×844 触摸上下文在真实数据上检查了书库/生词本/进度无横向溢出。fixture 书籍在结束时删除;截图见 .local/evidence/issue14-real-*.png、.local/evidence/issue14-mobile-*.png、issue14-theme-*.png、issue14-keyboard-review.png。临时用例运行后删除。

未验证:真实手机上的长按选择、手柄拖动、滚动惯性、软键盘遮挡与安全区表现无法由本机自动化产生——本单的触摸证据来自模拟设备;真机验证仍需人工完成,这一点在 #4、#11 就已记录,本单未能消除。也未做 Safari/Firefox 等其他引擎的验证(项目既有测试只在 Chromium 上跑)。

#15 交付、备份与恢复验证(2026-09-15)

本单无 schema 变化、无迁移:新增运维脚本、部署文档与演练证据。所有命令在仓库根执行;MySQL 客户端用 8.4(LEXGO_MYSQL_BIN=D:/mysql843/bin),因为本机 PATH 上是 5.7 客户端,install-check 会把它判为不达标。

运维命令

用途 命令
依赖与资源检查 python scripts/ops.py install-check
建空库并提示权限 python scripts/ops.py init-database --database <库名>
备份 python scripts/ops.py backup --out <目录>
恢复到空库 python scripts/ops.py restore --dump <备份.sql.gz> --database <库名> --confirm
校验(数据完整性,可选接口闭环) python scripts/ops.py verify --database <库名> [--manifest <manifest.json>] [--api http://127.0.0.1:<端口> --user <前缀> --password-env <变量名>]
干净实例两账号闭环演练 python scripts/ops.py smoke --api <url> --admin-user <账号> --user <前缀>
性能测量 python scripts/bench.py --database <库名> --api <url> --user <账号>

完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])

演练脚本(.local/tmp/issue15-drill.py,一次性工具)依次执行:

  1. 干净安装:init-database 建空库 → migrate(schema v7,14 张表)→ 确认「新库没有账号、没有业务数据」→ bootstrap 建管理员 → 再次 bootstrap 被拒绝(退出码 1,不覆盖管理员)→ 起独立 API 实例(:8010)。
  2. 两个演练账号走通闭环(ops.py smoke,19 项全过):管理员建 trial_a/trial_b → 新账号书库与进度为空 → 粘贴章节 → 章节处理完成 → 分词返回可点选词 → 词典查询返回明确状态 → 保存词义 → 另一账号查不到该词条 → 新词立刻到期 → 作答成功 → 重复作答不重复记账 → 完成章节只记已读 → 进度反映活动;B 读 A 的书 404、书库仍为空。
  3. 备份 lexgo_dev(约 12 MB gz):manifest 记录 schema 7、提交哈希、逐表行数、sha256;确认 manifest 无凭据字段;确认 dump 不含 CREATE DATABASE/USE(不会误写回源库)。
  4. 恢复到空库 lexgo_restore_drill 并自动校验(33 项全过):逐表行数与备份一致(sys_user 6、books 27、chapters 33、terms 13、term_reviews 13、review_answers 55、login_logs 192 等)→ 逐表内容校验和与源库一致(13 张表)→ 源库 lexgo_dev 未被改动。
  5. 安全边界:恢复默认拒绝覆盖已有库;拒绝写入系统库 mysql;不带 --confirm 直接拒绝。
  6. 恢复实例的两账号闭环与越权校验(:8011,41 项全过)——恢复后的账号仍可登录、词条与到期队列仍在、答题与完成章节保持幂等、B 无法读取或修改 A 的数据。
  7. 收尾:删除两个演练库,确认 lexgo_dev 与测试库保留,lexgo_dev 仍是 schema 7。

性能观察(scripts/bench.py,人造数据集,单机无并发)

数据集:1 个账号、20 本书 20 章(每章约 500 个词)、2000 词条、8000 条复习作答。环境:Windows 10(10.0.19044)、8 核、31.7 GB 内存、MySQL 8.4.3、Go 工具链由 scripts/server.py 固定为 1.26.5。

接口 请求数 p50 p95 最大 错误
POST /login 10 82.2 ms 93.3 ms 93.3 ms 0
GET /books 30 5.9 ms 18.1 ms 18.2 ms 0
GET /chapters/1 30 8.3 ms 20.1 ms 22.0 ms 0
GET /chapters/1/tokens 30 10.0 ms 23.6 ms 30.3 ms 0
GET /terms(第 1 页) 30 10.2 ms 20.4 ms 21.5 ms 0
GET /terms?query(搜索) 30 12.3 ms 24.9 ms 27.0 ms 0
GET /progress 30 15.7 ms 26.1 ms 29.1 ms 0
GET /reviews/queue 30 22.7 ms 32.3 ms 34.2 ms 0

这些是观察值,不是容量承诺:单机、单进程、无并发压力;登录接口每地址每分钟限 30 次,因此登录只测 10 次,该数字不代表登录吞吐。原始报告:.local/issue15-bench.json。

集成验证矩阵(复用既有 Go 测试,LEXGO_TEST_DB_NAME=lexgo_test_issue13 70 项全过)

验收要求 覆盖用例(全部通过)
越权与隔离 TestMySQLLibraryIsolationAndOwnership、TestMySQLTermIsolationAndInputRules、TestMySQLPhraseRulesAndIsolation、TestMySQLReviewAnswerErrorsAndOwnership、TestMySQLAccountIsolationAndRevocation
任务崩溃重试 TestMySQLIngestRecoveryWithoutRestart、TestMySQLIngestRecoveryAfterRestart、TestMySQLIngestAttemptsAreBoundedAndManualRetryRestarts、TestMySQLRetryAfterContentRestoredPublishesSameChapter、TestMySQLRecoverySkipsSupersededJobs
并发答题与并发删除 TestMySQLReviewConcurrentReplayOfOneAnswer、TestConcurrentDuplicateAccountHasOneWinner、TestMySQLConcurrentChapterDelete、TestMySQLDeleteDuringProcessing
长文与输入上限 TestMySQLPasteRejectsInvalidInputAndLimits、TestMySQLTextUploadKeepsChapterLimit、TestMySQLTextUploadRejectsInvalidSubmissions、TestMySQLTextUploadRejectsInvalidSubmissions
迁移与回退标记 TestMigrationRefusesUnownedOrUnsupportedSchema、TestMigrationCanResumeOwnedPartialSchema、全部 TestMigrationFrom*

治理与其它测试

python -m unittest discover -s tests 65 项通过(新增 9 项 tests/test_lexgo_ops.py,不需要数据库);harness.py check --strict 与 sync --check 通过。本单未改前端与后端业务代码,仍复跑 LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 70 项通过。

未验证

  • 真实回滚(升级后切回旧二进制)未演练:文档给出规则(迁移只新增对象,回退前写回版本号),但没有在真实实例上做过升级/回滚全过程。
  • 未验证 HTTPS、域名、反代配置、多机与灰度;本机演练只在 127.0.0.1 上起两个临时实例。
  • 未做定时备份、监控与告警;备份与恢复均为人工触发。
  • 大库恢复耗时与磁盘空间上限未测;演练库与生产库规模不同。
  • 附件(封面/音频,#21)尚未实现,恢复范围目前只覆盖数据库。

#15 交付、备份与恢复验证(2026-09-15)

本单无 schema 变化、无迁移:新增运维脚本、部署文档与演练证据。所有命令在仓库根执行;MySQL 客户端用 8.4(LEXGO_MYSQL_BIN=D:/mysql843/bin),因为本机 PATH 上是 5.7 客户端,install-check 会把它判为不达标。

运维命令

纯二进制路径(部署机不需要 Python):

用途 命令
迁移 / 初始化管理员 ./lexgo migrate / ./lexgo bootstrap
备份 ./lexgo backup --out <目录>
恢复到空库 ./lexgo restore --dump <备份.sql.gz> --database <库名> --confirm
校验 ./lexgo verify --database <库名> [--manifest <manifest.json>]

工具通道(开发便利与交叉验证):

用途 命令
依赖与资源检查 python scripts/ops.py install-check
建空库并提示权限 python scripts/ops.py init-database --database <库名>
备份 python scripts/ops.py backup --out <目录>
恢复到空库 python scripts/ops.py restore --dump <备份.sql.gz> --database <库名> --confirm
校验(数据完整性,可选接口闭环) python scripts/ops.py verify --database <库名> [--manifest <manifest.json>] [--api http://127.0.0.1:<端口> --user <前缀> --password-env <变量名>]
干净实例两账号闭环演练 python scripts/ops.py smoke --api <url> --admin-user <账号> --user <前缀>
性能测量 python scripts/bench.py --database <库名> --api <url> --user <账号>

两条通道的交叉验证(2026-09-15,19 项检查全部通过)

Go 二进制与 Python 工具各做一次备份,并互相恢复对方的备份:

  1. 两条通道的 manifest.json 字段齐备、逐表行数一致、schema_version 一致、都不含凭据字段;
  2. Go 产出的 dump 由 Python 恢复成功,Python 产出的 dump 由 Go 恢复成功(lexgo restore 输出 verified);
  3. Go 校验 Go 恢复出来的库、Python 校验 Python 恢复出来的库,逐表行数与 manifest 一致;
  4. 两个恢复实例的逐表内容校验和都与源库一致(13 张表),源库在过程中未被改动;
  5. lexgo verify 可以直接校验源库;
  6. 安全边界两条通道各自确认:Go 拒绝覆盖已有库、要求 --confirm,Python 拒绝系统库。

证据:.local/issue15-crosscheck-evidence.json。Go 侧另有 TestMySQLOpsBackupRestoreRoundTrip 集成用例(备份 → 恢复到空库 → 校验 → 拒绝二次覆盖 → 源库校验和不变)。

完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])

演练脚本(.local/tmp/issue15-drill.py,一次性工具)依次执行:

  1. 干净安装:init-database 建空库 → migrate(schema v7,14 张表)→ 确认「新库没有账号、没有业务数据」→ bootstrap 建管理员 → 再次 bootstrap 被拒绝(退出码 1,不覆盖管理员)→ 起独立 API 实例(:8010)。
  2. 两个演练账号走通闭环(ops.py smoke,19 项全过):管理员建 trial_a/trial_b → 新账号书库与进度为空 → 粘贴章节 → 章节处理完成 → 分词返回可点选词 → 词典查询返回明确状态 → 保存词义 → 另一账号查不到该词条 → 新词立刻到期 → 作答成功 → 重复作答不重复记账 → 完成章节只记已读 → 进度反映活动;B 读 A 的书 404、书库仍为空。
  3. 备份 lexgo_dev(约 12 MB gz):manifest 记录 schema 7、提交哈希、逐表行数、sha256;确认 manifest 无凭据字段;确认 dump 不含 CREATE DATABASE/USE(不会误写回源库)。
  4. 恢复到空库 lexgo_restore_drill 并自动校验(33 项全过):逐表行数与备份一致(sys_user 6、books 27、chapters 33、terms 13、term_reviews 13、review_answers 55、login_logs 192 等)→ 逐表内容校验和与源库一致(13 张表)→ 源库 lexgo_dev 未被改动。
  5. 安全边界:恢复默认拒绝覆盖已有库;拒绝写入系统库 mysql;不带 --confirm 直接拒绝。
  6. 恢复实例的两账号闭环与越权校验(:8011,41 项全过)——恢复后的账号仍可登录、词条与到期队列仍在、答题与完成章节保持幂等、B 无法读取或修改 A 的数据。
  7. 收尾:删除两个演练库,确认 lexgo_dev 与测试库保留,lexgo_dev 仍是 schema 7。

性能观察(scripts/bench.py,人造数据集,单机无并发)

数据集:1 个账号、20 本书 20 章(每章约 500 个词)、2000 词条、8000 条复习作答。环境:Windows 10(10.0.19044)、8 核、31.7 GB 内存、MySQL 8.4.3、Go 工具链由 scripts/server.py 固定为 1.26.5。

接口 请求数 p50 p95 最大 错误
POST /login 10 82.2 ms 93.3 ms 93.3 ms 0
GET /books 30 5.9 ms 18.1 ms 18.2 ms 0
GET /chapters/1 30 8.3 ms 20.1 ms 22.0 ms 0
GET /chapters/1/tokens 30 10.0 ms 23.6 ms 30.3 ms 0
GET /terms(第 1 页) 30 10.2 ms 20.4 ms 21.5 ms 0
GET /terms?query(搜索) 30 12.3 ms 24.9 ms 27.0 ms 0
GET /progress 30 15.7 ms 26.1 ms 29.1 ms 0
GET /reviews/queue 30 22.7 ms 32.3 ms 34.2 ms 0

这些是观察值,不是容量承诺:单机、单进程、无并发压力;登录接口每地址每分钟限 30 次,因此登录只测 10 次,该数字不代表登录吞吐。原始报告:.local/issue15-bench.json。

集成验证矩阵(复用既有 Go 测试,LEXGO_TEST_DB_NAME=lexgo_test_issue13 70 项全过)

验收要求 覆盖用例(全部通过)
越权与隔离 TestMySQLLibraryIsolationAndOwnership、TestMySQLTermIsolationAndInputRules、TestMySQLPhraseRulesAndIsolation、TestMySQLReviewAnswerErrorsAndOwnership、TestMySQLAccountIsolationAndRevocation
任务崩溃重试 TestMySQLIngestRecoveryWithoutRestart、TestMySQLIngestRecoveryAfterRestart、TestMySQLIngestAttemptsAreBoundedAndManualRetryRestarts、TestMySQLRetryAfterContentRestoredPublishesSameChapter、TestMySQLRecoverySkipsSupersededJobs
并发答题与并发删除 TestMySQLReviewConcurrentReplayOfOneAnswer、TestConcurrentDuplicateAccountHasOneWinner、TestMySQLConcurrentChapterDelete、TestMySQLDeleteDuringProcessing
长文与输入上限 TestMySQLPasteRejectsInvalidInputAndLimits、TestMySQLTextUploadKeepsChapterLimit、TestMySQLTextUploadRejectsInvalidSubmissions、TestMySQLTextUploadRejectsInvalidSubmissions
迁移与回退标记 TestMigrationRefusesUnownedOrUnsupportedSchema、TestMigrationCanResumeOwnedPartialSchema、全部 TestMigrationFrom*

治理与其它测试

python -m unittest discover -s tests 65 项通过(新增 9 项 tests/test_lexgo_ops.py,不需要数据库);harness.py check --strict 与 sync --check 通过。本单未改前端与后端业务代码,仍复跑 LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 70 项通过。

未验证

  • 真实回滚(升级后切回旧二进制)未演练:文档给出规则(迁移只新增对象,回退前写回版本号),但没有在真实实例上做过升级/回滚全过程。
  • 未验证 HTTPS、域名、反代配置、多机与灰度;本机演练只在 127.0.0.1 上起两个临时实例。
  • 未做定时备份、监控与告警;备份与恢复均为人工触发。
  • 大库恢复耗时与磁盘空间上限未测;演练库与生产库规模不同。
  • 附件(封面/音频,#21)尚未实现,恢复范围目前只覆盖数据库。

#15 交付、备份与恢复验证(2026-09-15)

本单无 schema 变化、无迁移:新增运维脚本、部署文档与演练证据。所有命令在仓库根执行;MySQL 客户端用 8.4(LEXGO_MYSQL_BIN=D:/mysql843/bin),因为本机 PATH 上是 5.7 客户端,install-check 会把它判为不达标。

运维命令

纯二进制路径(部署机不需要 Python):

用途 命令
迁移 / 初始化管理员 ./lexgo migrate / ./lexgo bootstrap
备份 ./lexgo backup --out <目录>
恢复到空库 ./lexgo restore --dump <备份.sql.gz> --database <库名> --confirm
校验 ./lexgo verify --database <库名> [--manifest <manifest.json>]

工具通道(开发便利与交叉验证):

用途 命令
依赖与资源检查 python scripts/ops.py install-check
建空库并提示权限 python scripts/ops.py init-database --database <库名>
备份 python scripts/ops.py backup --out <目录>
恢复到空库 python scripts/ops.py restore --dump <备份.sql.gz> --database <库名> --confirm
校验(数据完整性,可选接口闭环) python scripts/ops.py verify --database <库名> [--manifest <manifest.json>] [--api http://127.0.0.1:<端口> --user <前缀> --password-env <变量名>]
干净实例两账号闭环演练 python scripts/ops.py smoke --api <url> --admin-user <账号> --user <前缀>
性能测量 python scripts/bench.py --database <库名> --api <url> --user <账号>

纯二进制安装演练(2026-09-15,9 项检查全部通过)

不使用 Python 执行安装步骤:建空库 → lexgo migrate(v7,14 张表)→ 确认空库无账号 → lexgo bootstrap 建管理员 → 再次 bootstrap 被拒绝 → lexgo verify 通过 → lexgo serve 自身健康检查通过 → 两个演练账号走通学习闭环(19 项)→ 删除演练库。接口层的两账号闭环由工具通道驱动(它需要发 HTTP 请求),这一点在部署页已写明。证据:.local/issue15-binary-drill-evidence.json。

两条通道的交叉验证(2026-09-15,19 项检查全部通过)

Go 二进制与 Python 工具各做一次备份,并互相恢复对方的备份:

  1. 两条通道的 manifest.json 字段齐备、逐表行数一致、schema_version 一致、都不含凭据字段;
  2. Go 产出的 dump 由 Python 恢复成功,Python 产出的 dump 由 Go 恢复成功(lexgo restore 输出 verified);
  3. Go 校验 Go 恢复出来的库、Python 校验 Python 恢复出来的库,逐表行数与 manifest 一致;
  4. 两个恢复实例的逐表内容校验和都与源库一致(13 张表),源库在过程中未被改动;
  5. lexgo verify 可以直接校验源库;
  6. 安全边界两条通道各自确认:Go 拒绝覆盖已有库、要求 --confirm,Python 拒绝系统库。

证据:.local/issue15-crosscheck-evidence.json。Go 侧另有 TestMySQLOpsBackupRestoreRoundTrip 集成用例(备份 → 恢复到空库 → 校验 → 拒绝二次覆盖 → 源库校验和不变)。

完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])

演练脚本(.local/tmp/issue15-drill.py,一次性工具)依次执行:

  1. 干净安装:init-database 建空库 → migrate(schema v7,14 张表)→ 确认「新库没有账号、没有业务数据」→ bootstrap 建管理员 → 再次 bootstrap 被拒绝(退出码 1,不覆盖管理员)→ 起独立 API 实例(:8010)。
  2. 两个演练账号走通闭环(ops.py smoke,19 项全过):管理员建 trial_a/trial_b → 新账号书库与进度为空 → 粘贴章节 → 章节处理完成 → 分词返回可点选词 → 词典查询返回明确状态 → 保存词义 → 另一账号查不到该词条 → 新词立刻到期 → 作答成功 → 重复作答不重复记账 → 完成章节只记已读 → 进度反映活动;B 读 A 的书 404、书库仍为空。
  3. 备份 lexgo_dev(约 12 MB gz):manifest 记录 schema 7、提交哈希、逐表行数、sha256;确认 manifest 无凭据字段;确认 dump 不含 CREATE DATABASE/USE(不会误写回源库)。
  4. 恢复到空库 lexgo_restore_drill 并自动校验(33 项全过):逐表行数与备份一致(sys_user 6、books 27、chapters 33、terms 13、term_reviews 13、review_answers 55、login_logs 192 等)→ 逐表内容校验和与源库一致(13 张表)→ 源库 lexgo_dev 未被改动。
  5. 安全边界:恢复默认拒绝覆盖已有库;拒绝写入系统库 mysql;不带 --confirm 直接拒绝。
  6. 恢复实例的两账号闭环与越权校验(:8011,41 项全过)——恢复后的账号仍可登录、词条与到期队列仍在、答题与完成章节保持幂等、B 无法读取或修改 A 的数据。
  7. 收尾:删除两个演练库,确认 lexgo_dev 与测试库保留,lexgo_dev 仍是 schema 7。

性能观察(scripts/bench.py,人造数据集,单机无并发)

数据集:1 个账号、20 本书 20 章(每章约 500 个词)、2000 词条、8000 条复习作答。环境:Windows 10(10.0.19044)、8 核、31.7 GB 内存、MySQL 8.4.3、Go 工具链由 scripts/server.py 固定为 1.26.5。

接口 请求数 p50 p95 最大 错误
POST /login 10 82.2 ms 93.3 ms 93.3 ms 0
GET /books 30 5.9 ms 18.1 ms 18.2 ms 0
GET /chapters/1 30 8.3 ms 20.1 ms 22.0 ms 0
GET /chapters/1/tokens 30 10.0 ms 23.6 ms 30.3 ms 0
GET /terms(第 1 页) 30 10.2 ms 20.4 ms 21.5 ms 0
GET /terms?query(搜索) 30 12.3 ms 24.9 ms 27.0 ms 0
GET /progress 30 15.7 ms 26.1 ms 29.1 ms 0
GET /reviews/queue 30 22.7 ms 32.3 ms 34.2 ms 0

这些是观察值,不是容量承诺:单机、单进程、无并发压力;登录接口每地址每分钟限 30 次,因此登录只测 10 次,该数字不代表登录吞吐。原始报告:.local/issue15-bench.json。

集成验证矩阵(复用既有 Go 测试,LEXGO_TEST_DB_NAME=lexgo_test_issue13 70 项全过)

验收要求 覆盖用例(全部通过)
越权与隔离 TestMySQLLibraryIsolationAndOwnership、TestMySQLTermIsolationAndInputRules、TestMySQLPhraseRulesAndIsolation、TestMySQLReviewAnswerErrorsAndOwnership、TestMySQLAccountIsolationAndRevocation
任务崩溃重试 TestMySQLIngestRecoveryWithoutRestart、TestMySQLIngestRecoveryAfterRestart、TestMySQLIngestAttemptsAreBoundedAndManualRetryRestarts、TestMySQLRetryAfterContentRestoredPublishesSameChapter、TestMySQLRecoverySkipsSupersededJobs
并发答题与并发删除 TestMySQLReviewConcurrentReplayOfOneAnswer、TestConcurrentDuplicateAccountHasOneWinner、TestMySQLConcurrentChapterDelete、TestMySQLDeleteDuringProcessing
长文与输入上限 TestMySQLPasteRejectsInvalidInputAndLimits、TestMySQLTextUploadKeepsChapterLimit、TestMySQLTextUploadRejectsInvalidSubmissions、TestMySQLTextUploadRejectsInvalidSubmissions
迁移与回退标记 TestMigrationRefusesUnownedOrUnsupportedSchema、TestMigrationCanResumeOwnedPartialSchema、全部 TestMigrationFrom*

治理与其它测试

python -m unittest discover -s tests 65 项通过(新增 9 项 tests/test_lexgo_ops.py,不需要数据库);harness.py check --strict 与 sync --check 通过。本单未改前端与后端业务代码,仍复跑 LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 70 项通过。

未验证

  • 真实回滚(升级后切回旧二进制)未演练:文档给出规则(迁移只新增对象,回退前写回版本号),但没有在真实实例上做过升级/回滚全过程。
  • 未验证 HTTPS、域名、反代配置、多机与灰度;本机演练只在 127.0.0.1 上起两个临时实例。
  • 未做定时备份、监控与告警;备份与恢复均为人工触发。
  • 大库恢复耗时与磁盘空间上限未测;演练库与生产库规模不同。
  • 附件(封面/音频,#21)尚未实现,恢复范围目前只覆盖数据库。

#21 附件功能验证(2026-09-15)

迁移

schema v7 → v8:新增 lexgo_book_attachments 与 lexgo_playback_positions 两张表,不修改既有表。开发库迁移前后逐表业务计数一致,schema 记到 8,两张新表为空(快照 .local/issue13-before21.json、.local/issue13-after21.json);回退时把 lexgo_schema.version 写回 7 并恢复上一版二进制(备份 .local/lexgo-pre-issue21.exe)。

命令 结果
go vet ./... 通过,gofmt -l 无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 86 项顶层用例全部通过、0 跳过(本单新增 7 项:类型嗅探、上限与像素、WebP 维度、位置边界、上传/读取/Range/替换/移除/级联的集成用例、v7→v8 迁移)
cd learner:npx vitest --run / npx vue-tsc --build / npx pnpm run build / npx playwright test 157 项单测、类型检查、构建、25 项 E2E(桌面 21 + 移动 4)全部通过;本单新增 10 单测与 3 项 E2E
cd admin:npx pnpm test / npx pnpm lint 31 项与 lint 通过(管理端无改动)
python -m unittest discover -s tests / harness.py check --strict / sync --check 65 项、严格检查、镜像一致

真实链路(41 项检查全部通过)

真实 API+真实 MySQL:上传真实 PNG(461 字节)与真实三秒 MP3(ffmpeg 生成,18407 字节)→ 数据库中的 byte_size 与 OCTET_LENGTH(bytes) 一致、音频 sha256 与上传文件一致 → 书籍详情与书库列表报告 attachments、coverVersion、hasAudio → 封面按字节返回且带 ETag(If-None-Match 得 304)→ 音频按字节返回、Accept-Ranges 存在、Range: bytes=100-999 得 206 且切片正确、不可满足的 Range 得 416 → 越权读取/替换/上报位置全部 404、匿名 401 → SVG 改名封面、文本改名音频均 400,21 MiB 音频 413,且被拒绝后原文件仍在 → 位置写入与回读、负值 400、无音频的书 404 → 替换音频后位置归零且仍只有一行 → 移除音频、删除书籍后附件与位置级联清空。证据 .local/issue21-api-evidence.json。

恢复演练(含附件,22 项检查全部通过)

在 #15 的恢复契约上扩展:建一本带真实封面与音频的 fixture 书籍 → 用 Go 二进制 lexgo backup(manifest 已包含两张新表)→ 恢复到空库 lexgo_test_restore_attach → lexgo verify --manifest 通过且新增的「附件都指向存在的书」「播放位置都指向存在的书」检查在内 → 15 张表的逐表内容校验和与源库一致 → 用恢复库起第二个实例:同一账号可登录、书籍仍报告两个附件、位置仍是恢复前的值、音频与封面按字节相同地返回、恢复实例上 Range 仍得 206、越权仍 404 → 删除演练库与 fixture 书籍,开发库仍是 schema 8。证据 .local/issue21-restore-evidence.json。

顺带修掉的既有问题

e2e/phrase.spec.ts 的连续选词用例在并行全量跑时约 6 次失败 2 次(合成鼠标拖拽偶尔少选一个词)。修法:每次重试都重新读取词的位置(面板打开会改变布局,缓存的坐标会一直拖错)、松开前留 80ms 让浏览器扩展选区、把重试条件改成断言期望的词数而不是「面板出现了」、重试前关掉部分选中的面板。修完连续 6 次全量跑全部通过;若选择逻辑真的退化,4 次重试仍会失败,不会掩盖真实问题。

未验证

  • 浏览器是否真的解码并播放音频:本机验证用桩替换了媒体元素(合成的 MP3 帧不足以证明解码)。真实 MP3 的听感、手机扬声器/蓝牙、后台播放与锁屏控制需要人工确认。
  • 未验证 Safari/Firefox(项目既有测试只在 Chromium 上跑);WebP 只验证了容器维度解析,未在各浏览器实测显示。
  • 20 MiB 上限下的播放启动延迟未测;附件进库对 dump 体积与恢复耗时的影响只在小数据量下观察。
  • 未做音频转码与格式探测,非 MP3 一律拒绝。

#37 章级附件验证(2026-09-15)

迁移

schema v8 → v9:新增 lexgo_chapter_attachments 与 lexgo_chapter_playback_positions,并执行一条幂等清理语句删除 lexgo_book_attachments 中 kind='audio' 的历史行(测试期数据,不做存量迁移)。开发库迁移前后逐表业务计数一致,schema 记到 9,两张新表为空、书级音频行为 0(快照 .local/issue13-before37.json、.local/issue13-after37.json)。回退:把 lexgo_schema.version 写回 8 并恢复 .local/lexgo-pre-issue37.exe,但被清空的书级音频测试数据找不回。

命令 结果
go vet ./... / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 87 项顶层用例通过、0 跳过(本单新增/改写:章级附件与位置的完整集成用例、封面仍为书级、上传请求形态校验、v8→v9 迁移与回退重升级、v7→v8 测试改为用原始 SQL 写已退役的表)
cd learner:vitest / vue-tsc / build / playwright test 157 项单测、类型检查、构建、26 项 E2E(桌面 22 + 移动 4)全部通过
治理 / 镜像 65 项、check --strict、sync --check 通过
真实 API+MySQL 52 项检查通过(见下)
#15 恢复演练(第三次) 22 项检查通过(见下)

真实链路(52 项)

用一本两章的虚构书验证「附件属于章而不是书」:只给第一章上传真实的 PNG 插图与三秒 MP3 音频 → 数据库里两张行与章节对应、byte_size 与 OCTET_LENGTH(bytes) 一致、音频 sha256 与上传文件一致、书级音频行为 0 → 章节列表与阅读器都带 illustrationVersion/audioVersion,第二章为空串且位置为 0 → 插图按字节返回并带 ETag(If-None-Match 得 304)→ 音频按字节返回、Accept-Ranges 存在、Range: bytes=200-399 得 206 且切片正确、不可满足的 Range 得 416 → 书级音频与播放接口已下线(404) → 位置写入后章节列表回读一致、第二章仍为 0、负值 400、无音频的章 404 → 替换音频后位置归零且仍一行 → SVG 伪装插图、文本伪装音频 400、21 MiB 音频 413 且原文件保留 → 越权读取/替换/上报 404、匿名 401 → 移除音频清掉位置但插图不受影响 → 删除章节与删除书籍级联清空。证据 .local/issue37-api-evidence.json。

恢复演练(第三次,22 项)

带章级附件的 fixture → Go 二进制 lexgo backup(manifest 已含两张新表)→ 恢复到空库 lexgo_test_restore_chapter → lexgo verify --manifest 通过且包含「章节附件都指向存在的章节」「章节播放位置都指向存在的章节」→ 17 张表逐表内容校验和与源库一致 → 用恢复库起实例:同一账号可登录、章节仍报告两个文件、位置仍是恢复前的值、音频与插图按字节相同地返回、Range 仍得 206、越权仍 404 → 演练库与 fixture 删除,开发库仍是 schema 9。证据 .local/issue37-restore-evidence.json。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

附件并入章节编辑对话框(2026-09-15,按用户要求)

章节列表每行只留一个「编辑」;原「章节附件」对话框被移除,插图与音频的上传/替换/移除移入章节编辑对话框(与标题、正文同处),对话框内明确写出「标题与正文点保存后生效;插图与音频选中后立即上传,取消不会撤销」。后端与 schema 仍无变化。验证:学习端 157 单测与 26 项 E2E 全通过(新断言:章节行只有一个按钮;编辑对话框同时含标题、正文、插图与音频控件与生效时机说明;上传后仍可用「保存章节」提交标题与正文);真实链路确认章节行按钮数为 1、对话框中四类控件齐备、在编辑框内上传后状态立即变为「已设置」而标题仍等待保存、取消后进入阅读页仍为缩略图+弹窗,最后移除 fixture 插图(截图 issue37-real-chapter-editor.png)。

对话框浮层缺陷修复(2026-09-15,用户发现)

用户报告「编辑区显示在章节列表下面」。查明原因:learner/src/main.ts 从未导入 element-plus/es/components/dialog/style/css 与 message-box/style/css,因此自 #10 起所有 ElDialog 与 ElMessageBox 都退化成文档流中的普通块(探针实测:移除导入时对话框 position: static、top 正好等于章节列表底部;恢复导入后 .el-overlay 为 fixed、对话框居中且遮罩为 rgba(0,0,0,0.5))。修复:在 main.ts 补上两行样式导入。

验证盲区:此前所有 UI 验证只断言「元素可见 / DOM 结构」,toBeVisible() 对普通块同样成立,所以单元测试与 E2E 全绿也没暴露它。现已补长期回归:learner/e2e/overlay.ts 提供 expectModalOverlay 与 expectMessageBoxOverlay,断言对话框位于 .el-overlay(position: fixed)、位置在视口内且横向不溢出;attachments.spec.ts(章节编辑对话框、插图弹窗)与 edit.spec.ts(书名对话框、删除确认)都调用它。真实链路再次确认:章节编辑对话框与删除确认均为固定浮层(截图 issue37-real-dialog-modal.png、issue37-real-confirm-modal.png)。

作者字段与编辑对话框紧凑化(2026-09-15,按用户要求)

按用户要求:①「章节标题」标签与输入框同行,下面新增「作者」标签与输入框;②精简插图/音频的提示文字,把高度让给正文编辑框。

  • schema v10:lexgo_chapters 增加 author VARCHAR(120) NOT NULL DEFAULT ''。由 Go 条件步骤 addChapterAuthorColumn(先查 information_schema 再 ALTER TABLE)在语句列表之后执行,保持「部分迁移可重试 / 回退标记后可重新升级」;新建库的 v3 语句也带该列。开发库迁移后 schema = 10,既有 34 个章节的 author 均为空串(无数据变化)。
  • 接口:章节编辑接受 author(可选、trim、≤120 字符、空串清空),ChapterSummary/ChapterSource 都返回它;仅改作者不会重新处理章节(状态保持不变)。
  • 界面:.field-row 把标签与输入框放同一行(标题、作者各一行);正文 textarea 提升到 18 行;插图与音频压成各一两行提示(规格写在下方一行);阅读页在标题下显示非空作者(data-testid="chapter-author-line")。
  • 验证:Go 89 项顶层用例(新增 TestMySQLChapterAuthorRoundTrip 与 TestMigrationFromV9AddsChapterAuthor,后者覆盖「旧库缺列 → 迁移补列 → 回退标记 → 重新升级」);学习端 157 单测与 26 项 E2E 通过;真实链路实测:标题标签与输入框中线偏差 <8px、作者行位于标题下方且标签同行、正文高度 415px(原约 250px)、保存作者后阅读页显示、清空后该行消失(截图 issue37-real-editor-layout.png、issue37-real-reader-author.png)。

书级作者与书籍页精简(2026-09-15,按用户要求)

按用户要求:①书籍页去掉「封面」「书籍封面」「JPG…4096×4096」「音频与插图按章节设置…」四段文字,把空间让给章节列表;②「编辑书名」改为**「编辑书籍」并可编辑书名与作者**;③书名右侧显示当前书籍作者。

  • schema v11:lexgo_books.author VARCHAR(120) NOT NULL DEFAULT '',复用 v10 的条件加列助手(改名 addAuthorColumn,表名取自本文件常量)。开发库迁移后 schema = 11,既有 25 本书的 author 均为空串;回退件 .local/lexgo-pre-issue37c.exe(回退写回版本号 10 即可)。
  • 接口:PATCH /api/v1/books/:id 接受可选 author(省略保留、空串清空、trim、≤120),BookSummary/BookRef 返回它;书籍编辑不再只改书名。
  • 界面:封面区块变成一行(预览 + 上传/替换 + 移除);.title-line 在书名右侧显示作者(未设置不显示);「编辑书籍」对话框用同行标签编辑书名与作者。
  • 验证:Go 91 项顶层用例(新增 TestMySQLBookAuthorRoundTrip 与 TestMigrationFromV10AddsBookAuthor);学习端 157 单测与 26 项 E2E 通过;真实链路实测封面行高 68px、章节列表紧随其后(间距 18px)、四段被删文字都不再出现、「编辑书籍」对话框含作者输入、保存后作者显示在书名右侧(同一行、位于书名之后)并在刷新后保留、清空后标签消失(截图 issue37-real-book-page.png)。

验收后的最终状态(2026-09-16)

用户回复「#37 通过验收」;PR #38 已 fast-forward-only 合入 main(merge commit 728f7d6),工单关闭。最终 schema 为 v11(v9 章级附件、v10 章节作者、v11 书籍作者)。经确认未实现的两项:书库卡片显示书级作者、章节作者留空时回落显示书级作者;数据层已就绪(BookSummary/BookRef 与 ChapterSummary 都返回作者),需要时另开小工单即可。

未验证

  • 真机与真实浏览器解码播放(媒体元素在测试中被桩替换)、真机听感与兼容性仍未验证(#21 起的既有缺口)。
  • Safari/Firefox 未测;渐进式流式播放仍未做(服务端 Range 已实现)。
  • 迁移会清空书级音频测试数据且回退到 v8 找不回;当前无真实用户,工单已记录。
  • 已有书籍/章节的存量内容不受影响,但没有做「把既有书级音频迁到章级」的转换(无真实数据)。

#37 章级附件验证(2026-09-15)

迁移

schema v8 → v9:新增 lexgo_chapter_attachments 与 lexgo_chapter_playback_positions,并执行一条幂等清理语句删除 lexgo_book_attachments 中 kind='audio' 的历史行(测试期数据,不做存量迁移)。开发库迁移前后逐表业务计数一致,schema 记到 9,两张新表为空、书级音频行为 0(快照 .local/issue13-before37.json、.local/issue13-after37.json)。回退:把 lexgo_schema.version 写回 8 并恢复 .local/lexgo-pre-issue37.exe,但被清空的书级音频测试数据找不回。

命令 结果
go vet ./... / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 87 项顶层用例通过、0 跳过(本单新增/改写:章级附件与位置的完整集成用例、封面仍为书级、上传请求形态校验、v8→v9 迁移与回退重升级、v7→v8 测试改为用原始 SQL 写已退役的表)
cd learner:vitest / vue-tsc / build / playwright test 157 项单测、类型检查、构建、26 项 E2E(桌面 22 + 移动 4)全部通过
治理 / 镜像 65 项、check --strict、sync --check 通过
真实 API+MySQL 52 项检查通过(见下)
#15 恢复演练(第三次) 22 项检查通过(见下)

真实链路(52 项)

用一本两章的虚构书验证「附件属于章而不是书」:只给第一章上传真实的 PNG 插图与三秒 MP3 音频 → 数据库里两张行与章节对应、byte_size 与 OCTET_LENGTH(bytes) 一致、音频 sha256 与上传文件一致、书级音频行为 0 → 章节列表与阅读器都带 illustrationVersion/audioVersion,第二章为空串且位置为 0 → 插图按字节返回并带 ETag(If-None-Match 得 304)→ 音频按字节返回、Accept-Ranges 存在、Range: bytes=200-399 得 206 且切片正确、不可满足的 Range 得 416 → 书级音频与播放接口已下线(404) → 位置写入后章节列表回读一致、第二章仍为 0、负值 400、无音频的章 404 → 替换音频后位置归零且仍一行 → SVG 伪装插图、文本伪装音频 400、21 MiB 音频 413 且原文件保留 → 越权读取/替换/上报 404、匿名 401 → 移除音频清掉位置但插图不受影响 → 删除章节与删除书籍级联清空。证据 .local/issue37-api-evidence.json。

恢复演练(第三次,22 项)

带章级附件的 fixture → Go 二进制 lexgo backup(manifest 已含两张新表)→ 恢复到空库 lexgo_test_restore_chapter → lexgo verify --manifest 通过且包含「章节附件都指向存在的章节」「章节播放位置都指向存在的章节」→ 17 张表逐表内容校验和与源库一致 → 用恢复库起实例:同一账号可登录、章节仍报告两个文件、位置仍是恢复前的值、音频与插图按字节相同地返回、Range 仍得 206、越权仍 404 → 演练库与 fixture 删除,开发库仍是 schema 9。证据 .local/issue37-restore-evidence.json。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

未验证

  • 真机与真实浏览器解码播放(媒体元素在测试中被桩替换)、真机听感与兼容性仍未验证(#21 起的既有缺口)。
  • Safari/Firefox 未测;渐进式流式播放仍未做(服务端 Range 已实现)。
  • 迁移会清空书级音频测试数据且回退到 v8 找不回;当前无真实用户,工单已记录。
  • 已有书籍/章节的存量内容不受影响,但没有做「把既有书级音频迁到章级」的转换(无真实数据)。

#37 章级附件验证(2026-09-15)

迁移

schema v8 → v9:新增 lexgo_chapter_attachments 与 lexgo_chapter_playback_positions,并执行一条幂等清理语句删除 lexgo_book_attachments 中 kind='audio' 的历史行(测试期数据,不做存量迁移)。开发库迁移前后逐表业务计数一致,schema 记到 9,两张新表为空、书级音频行为 0(快照 .local/issue13-before37.json、.local/issue13-after37.json)。回退:把 lexgo_schema.version 写回 8 并恢复 .local/lexgo-pre-issue37.exe,但被清空的书级音频测试数据找不回。

命令 结果
go vet ./... / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 87 项顶层用例通过、0 跳过(本单新增/改写:章级附件与位置的完整集成用例、封面仍为书级、上传请求形态校验、v8→v9 迁移与回退重升级、v7→v8 测试改为用原始 SQL 写已退役的表)
cd learner:vitest / vue-tsc / build / playwright test 157 项单测、类型检查、构建、26 项 E2E(桌面 22 + 移动 4)全部通过
治理 / 镜像 65 项、check --strict、sync --check 通过
真实 API+MySQL 52 项检查通过(见下)
#15 恢复演练(第三次) 22 项检查通过(见下)

真实链路(52 项)

用一本两章的虚构书验证「附件属于章而不是书」:只给第一章上传真实的 PNG 插图与三秒 MP3 音频 → 数据库里两张行与章节对应、byte_size 与 OCTET_LENGTH(bytes) 一致、音频 sha256 与上传文件一致、书级音频行为 0 → 章节列表与阅读器都带 illustrationVersion/audioVersion,第二章为空串且位置为 0 → 插图按字节返回并带 ETag(If-None-Match 得 304)→ 音频按字节返回、Accept-Ranges 存在、Range: bytes=200-399 得 206 且切片正确、不可满足的 Range 得 416 → 书级音频与播放接口已下线(404) → 位置写入后章节列表回读一致、第二章仍为 0、负值 400、无音频的章 404 → 替换音频后位置归零且仍一行 → SVG 伪装插图、文本伪装音频 400、21 MiB 音频 413 且原文件保留 → 越权读取/替换/上报 404、匿名 401 → 移除音频清掉位置但插图不受影响 → 删除章节与删除书籍级联清空。证据 .local/issue37-api-evidence.json。

恢复演练(第三次,22 项)

带章级附件的 fixture → Go 二进制 lexgo backup(manifest 已含两张新表)→ 恢复到空库 lexgo_test_restore_chapter → lexgo verify --manifest 通过且包含「章节附件都指向存在的章节」「章节播放位置都指向存在的章节」→ 17 张表逐表内容校验和与源库一致 → 用恢复库起实例:同一账号可登录、章节仍报告两个文件、位置仍是恢复前的值、音频与插图按字节相同地返回、Range 仍得 206、越权仍 404 → 演练库与 fixture 删除,开发库仍是 schema 9。证据 .local/issue37-restore-evidence.json。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

附件并入章节编辑对话框(2026-09-15,按用户要求)

章节列表每行只留一个「编辑」;原「章节附件」对话框被移除,插图与音频的上传/替换/移除移入章节编辑对话框(与标题、正文同处),对话框内明确写出「标题与正文点保存后生效;插图与音频选中后立即上传,取消不会撤销」。后端与 schema 仍无变化。验证:学习端 157 单测与 26 项 E2E 全通过(新断言:章节行只有一个按钮;编辑对话框同时含标题、正文、插图与音频控件与生效时机说明;上传后仍可用「保存章节」提交标题与正文);真实链路确认章节行按钮数为 1、对话框中四类控件齐备、在编辑框内上传后状态立即变为「已设置」而标题仍等待保存、取消后进入阅读页仍为缩略图+弹窗,最后移除 fixture 插图(截图 issue37-real-chapter-editor.png)。

未验证

  • 真机与真实浏览器解码播放(媒体元素在测试中被桩替换)、真机听感与兼容性仍未验证(#21 起的既有缺口)。
  • Safari/Firefox 未测;渐进式流式播放仍未做(服务端 Range 已实现)。
  • 迁移会清空书级音频测试数据且回退到 v8 找不回;当前无真实用户,工单已记录。
  • 已有书籍/章节的存量内容不受影响,但没有做「把既有书级音频迁到章级」的转换(无真实数据)。

#37 章级附件验证(2026-09-15)

迁移

schema v8 → v9:新增 lexgo_chapter_attachments 与 lexgo_chapter_playback_positions,并执行一条幂等清理语句删除 lexgo_book_attachments 中 kind='audio' 的历史行(测试期数据,不做存量迁移)。开发库迁移前后逐表业务计数一致,schema 记到 9,两张新表为空、书级音频行为 0(快照 .local/issue13-before37.json、.local/issue13-after37.json)。回退:把 lexgo_schema.version 写回 8 并恢复 .local/lexgo-pre-issue37.exe,但被清空的书级音频测试数据找不回。

命令 结果
go vet ./... / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 87 项顶层用例通过、0 跳过(本单新增/改写:章级附件与位置的完整集成用例、封面仍为书级、上传请求形态校验、v8→v9 迁移与回退重升级、v7→v8 测试改为用原始 SQL 写已退役的表)
cd learner:vitest / vue-tsc / build / playwright test 157 项单测、类型检查、构建、26 项 E2E(桌面 22 + 移动 4)全部通过
治理 / 镜像 65 项、check --strict、sync --check 通过
真实 API+MySQL 52 项检查通过(见下)
#15 恢复演练(第三次) 22 项检查通过(见下)

真实链路(52 项)

用一本两章的虚构书验证「附件属于章而不是书」:只给第一章上传真实的 PNG 插图与三秒 MP3 音频 → 数据库里两张行与章节对应、byte_size 与 OCTET_LENGTH(bytes) 一致、音频 sha256 与上传文件一致、书级音频行为 0 → 章节列表与阅读器都带 illustrationVersion/audioVersion,第二章为空串且位置为 0 → 插图按字节返回并带 ETag(If-None-Match 得 304)→ 音频按字节返回、Accept-Ranges 存在、Range: bytes=200-399 得 206 且切片正确、不可满足的 Range 得 416 → 书级音频与播放接口已下线(404) → 位置写入后章节列表回读一致、第二章仍为 0、负值 400、无音频的章 404 → 替换音频后位置归零且仍一行 → SVG 伪装插图、文本伪装音频 400、21 MiB 音频 413 且原文件保留 → 越权读取/替换/上报 404、匿名 401 → 移除音频清掉位置但插图不受影响 → 删除章节与删除书籍级联清空。证据 .local/issue37-api-evidence.json。

恢复演练(第三次,22 项)

带章级附件的 fixture → Go 二进制 lexgo backup(manifest 已含两张新表)→ 恢复到空库 lexgo_test_restore_chapter → lexgo verify --manifest 通过且包含「章节附件都指向存在的章节」「章节播放位置都指向存在的章节」→ 17 张表逐表内容校验和与源库一致 → 用恢复库起实例:同一账号可登录、章节仍报告两个文件、位置仍是恢复前的值、音频与插图按字节相同地返回、Range 仍得 206、越权仍 404 → 演练库与 fixture 删除,开发库仍是 schema 9。证据 .local/issue37-restore-evidence.json。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

附件并入章节编辑对话框(2026-09-15,按用户要求)

章节列表每行只留一个「编辑」;原「章节附件」对话框被移除,插图与音频的上传/替换/移除移入章节编辑对话框(与标题、正文同处),对话框内明确写出「标题与正文点保存后生效;插图与音频选中后立即上传,取消不会撤销」。后端与 schema 仍无变化。验证:学习端 157 单测与 26 项 E2E 全通过(新断言:章节行只有一个按钮;编辑对话框同时含标题、正文、插图与音频控件与生效时机说明;上传后仍可用「保存章节」提交标题与正文);真实链路确认章节行按钮数为 1、对话框中四类控件齐备、在编辑框内上传后状态立即变为「已设置」而标题仍等待保存、取消后进入阅读页仍为缩略图+弹窗,最后移除 fixture 插图(截图 issue37-real-chapter-editor.png)。

对话框浮层缺陷修复(2026-09-15,用户发现)

用户报告「编辑区显示在章节列表下面」。查明原因:learner/src/main.ts 从未导入 element-plus/es/components/dialog/style/css 与 message-box/style/css,因此自 #10 起所有 ElDialog 与 ElMessageBox 都退化成文档流中的普通块(探针实测:移除导入时对话框 position: static、top 正好等于章节列表底部;恢复导入后 .el-overlay 为 fixed、对话框居中且遮罩为 rgba(0,0,0,0.5))。修复:在 main.ts 补上两行样式导入。

验证盲区:此前所有 UI 验证只断言「元素可见 / DOM 结构」,toBeVisible() 对普通块同样成立,所以单元测试与 E2E 全绿也没暴露它。现已补长期回归:learner/e2e/overlay.ts 提供 expectModalOverlay 与 expectMessageBoxOverlay,断言对话框位于 .el-overlay(position: fixed)、位置在视口内且横向不溢出;attachments.spec.ts(章节编辑对话框、插图弹窗)与 edit.spec.ts(书名对话框、删除确认)都调用它。真实链路再次确认:章节编辑对话框与删除确认均为固定浮层(截图 issue37-real-dialog-modal.png、issue37-real-confirm-modal.png)。

未验证

  • 真机与真实浏览器解码播放(媒体元素在测试中被桩替换)、真机听感与兼容性仍未验证(#21 起的既有缺口)。
  • Safari/Firefox 未测;渐进式流式播放仍未做(服务端 Range 已实现)。
  • 迁移会清空书级音频测试数据且回退到 v8 找不回;当前无真实用户,工单已记录。
  • 已有书籍/章节的存量内容不受影响,但没有做「把既有书级音频迁到章级」的转换(无真实数据)。

#37 章级附件验证(2026-09-15)

迁移

schema v8 → v9:新增 lexgo_chapter_attachments 与 lexgo_chapter_playback_positions,并执行一条幂等清理语句删除 lexgo_book_attachments 中 kind='audio' 的历史行(测试期数据,不做存量迁移)。开发库迁移前后逐表业务计数一致,schema 记到 9,两张新表为空、书级音频行为 0(快照 .local/issue13-before37.json、.local/issue13-after37.json)。回退:把 lexgo_schema.version 写回 8 并恢复 .local/lexgo-pre-issue37.exe,但被清空的书级音频测试数据找不回。

命令 结果
go vet ./... / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 87 项顶层用例通过、0 跳过(本单新增/改写:章级附件与位置的完整集成用例、封面仍为书级、上传请求形态校验、v8→v9 迁移与回退重升级、v7→v8 测试改为用原始 SQL 写已退役的表)
cd learner:vitest / vue-tsc / build / playwright test 157 项单测、类型检查、构建、26 项 E2E(桌面 22 + 移动 4)全部通过
治理 / 镜像 65 项、check --strict、sync --check 通过
真实 API+MySQL 52 项检查通过(见下)
#15 恢复演练(第三次) 22 项检查通过(见下)

真实链路(52 项)

用一本两章的虚构书验证「附件属于章而不是书」:只给第一章上传真实的 PNG 插图与三秒 MP3 音频 → 数据库里两张行与章节对应、byte_size 与 OCTET_LENGTH(bytes) 一致、音频 sha256 与上传文件一致、书级音频行为 0 → 章节列表与阅读器都带 illustrationVersion/audioVersion,第二章为空串且位置为 0 → 插图按字节返回并带 ETag(If-None-Match 得 304)→ 音频按字节返回、Accept-Ranges 存在、Range: bytes=200-399 得 206 且切片正确、不可满足的 Range 得 416 → 书级音频与播放接口已下线(404) → 位置写入后章节列表回读一致、第二章仍为 0、负值 400、无音频的章 404 → 替换音频后位置归零且仍一行 → SVG 伪装插图、文本伪装音频 400、21 MiB 音频 413 且原文件保留 → 越权读取/替换/上报 404、匿名 401 → 移除音频清掉位置但插图不受影响 → 删除章节与删除书籍级联清空。证据 .local/issue37-api-evidence.json。

恢复演练(第三次,22 项)

带章级附件的 fixture → Go 二进制 lexgo backup(manifest 已含两张新表)→ 恢复到空库 lexgo_test_restore_chapter → lexgo verify --manifest 通过且包含「章节附件都指向存在的章节」「章节播放位置都指向存在的章节」→ 17 张表逐表内容校验和与源库一致 → 用恢复库起实例:同一账号可登录、章节仍报告两个文件、位置仍是恢复前的值、音频与插图按字节相同地返回、Range 仍得 206、越权仍 404 → 演练库与 fixture 删除,开发库仍是 schema 9。证据 .local/issue37-restore-evidence.json。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

附件并入章节编辑对话框(2026-09-15,按用户要求)

章节列表每行只留一个「编辑」;原「章节附件」对话框被移除,插图与音频的上传/替换/移除移入章节编辑对话框(与标题、正文同处),对话框内明确写出「标题与正文点保存后生效;插图与音频选中后立即上传,取消不会撤销」。后端与 schema 仍无变化。验证:学习端 157 单测与 26 项 E2E 全通过(新断言:章节行只有一个按钮;编辑对话框同时含标题、正文、插图与音频控件与生效时机说明;上传后仍可用「保存章节」提交标题与正文);真实链路确认章节行按钮数为 1、对话框中四类控件齐备、在编辑框内上传后状态立即变为「已设置」而标题仍等待保存、取消后进入阅读页仍为缩略图+弹窗,最后移除 fixture 插图(截图 issue37-real-chapter-editor.png)。

对话框浮层缺陷修复(2026-09-15,用户发现)

用户报告「编辑区显示在章节列表下面」。查明原因:learner/src/main.ts 从未导入 element-plus/es/components/dialog/style/css 与 message-box/style/css,因此自 #10 起所有 ElDialog 与 ElMessageBox 都退化成文档流中的普通块(探针实测:移除导入时对话框 position: static、top 正好等于章节列表底部;恢复导入后 .el-overlay 为 fixed、对话框居中且遮罩为 rgba(0,0,0,0.5))。修复:在 main.ts 补上两行样式导入。

验证盲区:此前所有 UI 验证只断言「元素可见 / DOM 结构」,toBeVisible() 对普通块同样成立,所以单元测试与 E2E 全绿也没暴露它。现已补长期回归:learner/e2e/overlay.ts 提供 expectModalOverlay 与 expectMessageBoxOverlay,断言对话框位于 .el-overlay(position: fixed)、位置在视口内且横向不溢出;attachments.spec.ts(章节编辑对话框、插图弹窗)与 edit.spec.ts(书名对话框、删除确认)都调用它。真实链路再次确认:章节编辑对话框与删除确认均为固定浮层(截图 issue37-real-dialog-modal.png、issue37-real-confirm-modal.png)。

作者字段与编辑对话框紧凑化(2026-09-15,按用户要求)

按用户要求:①「章节标题」标签与输入框同行,下面新增「作者」标签与输入框;②精简插图/音频的提示文字,把高度让给正文编辑框。

  • schema v10:lexgo_chapters 增加 author VARCHAR(120) NOT NULL DEFAULT ''。由 Go 条件步骤 addChapterAuthorColumn(先查 information_schema 再 ALTER TABLE)在语句列表之后执行,保持「部分迁移可重试 / 回退标记后可重新升级」;新建库的 v3 语句也带该列。开发库迁移后 schema = 10,既有 34 个章节的 author 均为空串(无数据变化)。
  • 接口:章节编辑接受 author(可选、trim、≤120 字符、空串清空),ChapterSummary/ChapterSource 都返回它;仅改作者不会重新处理章节(状态保持不变)。
  • 界面:.field-row 把标签与输入框放同一行(标题、作者各一行);正文 textarea 提升到 18 行;插图与音频压成各一两行提示(规格写在下方一行);阅读页在标题下显示非空作者(data-testid="chapter-author-line")。
  • 验证:Go 89 项顶层用例(新增 TestMySQLChapterAuthorRoundTrip 与 TestMigrationFromV9AddsChapterAuthor,后者覆盖「旧库缺列 → 迁移补列 → 回退标记 → 重新升级」);学习端 157 单测与 26 项 E2E 通过;真实链路实测:标题标签与输入框中线偏差 <8px、作者行位于标题下方且标签同行、正文高度 415px(原约 250px)、保存作者后阅读页显示、清空后该行消失(截图 issue37-real-editor-layout.png、issue37-real-reader-author.png)。

未验证

  • 真机与真实浏览器解码播放(媒体元素在测试中被桩替换)、真机听感与兼容性仍未验证(#21 起的既有缺口)。
  • Safari/Firefox 未测;渐进式流式播放仍未做(服务端 Range 已实现)。
  • 迁移会清空书级音频测试数据且回退到 v8 找不回;当前无真实用户,工单已记录。
  • 已有书籍/章节的存量内容不受影响,但没有做「把既有书级音频迁到章级」的转换(无真实数据)。

#37 章级附件验证(2026-09-15)

迁移

schema v8 → v9:新增 lexgo_chapter_attachments 与 lexgo_chapter_playback_positions,并执行一条幂等清理语句删除 lexgo_book_attachments 中 kind='audio' 的历史行(测试期数据,不做存量迁移)。开发库迁移前后逐表业务计数一致,schema 记到 9,两张新表为空、书级音频行为 0(快照 .local/issue13-before37.json、.local/issue13-after37.json)。回退:把 lexgo_schema.version 写回 8 并恢复 .local/lexgo-pre-issue37.exe,但被清空的书级音频测试数据找不回。

命令 结果
go vet ./... / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 87 项顶层用例通过、0 跳过(本单新增/改写:章级附件与位置的完整集成用例、封面仍为书级、上传请求形态校验、v8→v9 迁移与回退重升级、v7→v8 测试改为用原始 SQL 写已退役的表)
cd learner:vitest / vue-tsc / build / playwright test 157 项单测、类型检查、构建、26 项 E2E(桌面 22 + 移动 4)全部通过
治理 / 镜像 65 项、check --strict、sync --check 通过
真实 API+MySQL 52 项检查通过(见下)
#15 恢复演练(第三次) 22 项检查通过(见下)

真实链路(52 项)

用一本两章的虚构书验证「附件属于章而不是书」:只给第一章上传真实的 PNG 插图与三秒 MP3 音频 → 数据库里两张行与章节对应、byte_size 与 OCTET_LENGTH(bytes) 一致、音频 sha256 与上传文件一致、书级音频行为 0 → 章节列表与阅读器都带 illustrationVersion/audioVersion,第二章为空串且位置为 0 → 插图按字节返回并带 ETag(If-None-Match 得 304)→ 音频按字节返回、Accept-Ranges 存在、Range: bytes=200-399 得 206 且切片正确、不可满足的 Range 得 416 → 书级音频与播放接口已下线(404) → 位置写入后章节列表回读一致、第二章仍为 0、负值 400、无音频的章 404 → 替换音频后位置归零且仍一行 → SVG 伪装插图、文本伪装音频 400、21 MiB 音频 413 且原文件保留 → 越权读取/替换/上报 404、匿名 401 → 移除音频清掉位置但插图不受影响 → 删除章节与删除书籍级联清空。证据 .local/issue37-api-evidence.json。

恢复演练(第三次,22 项)

带章级附件的 fixture → Go 二进制 lexgo backup(manifest 已含两张新表)→ 恢复到空库 lexgo_test_restore_chapter → lexgo verify --manifest 通过且包含「章节附件都指向存在的章节」「章节播放位置都指向存在的章节」→ 17 张表逐表内容校验和与源库一致 → 用恢复库起实例:同一账号可登录、章节仍报告两个文件、位置仍是恢复前的值、音频与插图按字节相同地返回、Range 仍得 206、越权仍 404 → 演练库与 fixture 删除,开发库仍是 schema 9。证据 .local/issue37-restore-evidence.json。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

插图交互修订(2026-09-15,按用户要求)

书籍页章节列表去掉插图列与列表预取;阅读页改为缩略图(高 120px 的按钮)并在点击后于弹窗中显示原图。后端与 schema 无变化。验证:学习端 157 单测与 26 项 E2E 通过(断言章节列表无 <img> 且不请求 /illustration、缩略图是按钮、点击后弹窗内为对象 URL 的图片);真实链路在 issue5_a 自己的一本书上完成——列表 <img> 为 0、缩略图 ≤130px、弹窗内图片大于缩略图且 naturalWidth > 0、Esc 关闭后正文仍可见,随后移除 fixture 插图复原开发数据(截图 issue37-real-book-no-image.png、issue37-real-reader-thumbnail.png、issue37-real-illustration-dialog.png)。示例地址 /books/5 属于其他账号,请求返回 404,实际使用该账号自己的书。

附件并入章节编辑对话框(2026-09-15,按用户要求)

章节列表每行只留一个「编辑」;原「章节附件」对话框被移除,插图与音频的上传/替换/移除移入章节编辑对话框(与标题、正文同处),对话框内明确写出「标题与正文点保存后生效;插图与音频选中后立即上传,取消不会撤销」。后端与 schema 仍无变化。验证:学习端 157 单测与 26 项 E2E 全通过(新断言:章节行只有一个按钮;编辑对话框同时含标题、正文、插图与音频控件与生效时机说明;上传后仍可用「保存章节」提交标题与正文);真实链路确认章节行按钮数为 1、对话框中四类控件齐备、在编辑框内上传后状态立即变为「已设置」而标题仍等待保存、取消后进入阅读页仍为缩略图+弹窗,最后移除 fixture 插图(截图 issue37-real-chapter-editor.png)。

对话框浮层缺陷修复(2026-09-15,用户发现)

用户报告「编辑区显示在章节列表下面」。查明原因:learner/src/main.ts 从未导入 element-plus/es/components/dialog/style/css 与 message-box/style/css,因此自 #10 起所有 ElDialog 与 ElMessageBox 都退化成文档流中的普通块(探针实测:移除导入时对话框 position: static、top 正好等于章节列表底部;恢复导入后 .el-overlay 为 fixed、对话框居中且遮罩为 rgba(0,0,0,0.5))。修复:在 main.ts 补上两行样式导入。

验证盲区:此前所有 UI 验证只断言「元素可见 / DOM 结构」,toBeVisible() 对普通块同样成立,所以单元测试与 E2E 全绿也没暴露它。现已补长期回归:learner/e2e/overlay.ts 提供 expectModalOverlay 与 expectMessageBoxOverlay,断言对话框位于 .el-overlay(position: fixed)、位置在视口内且横向不溢出;attachments.spec.ts(章节编辑对话框、插图弹窗)与 edit.spec.ts(书名对话框、删除确认)都调用它。真实链路再次确认:章节编辑对话框与删除确认均为固定浮层(截图 issue37-real-dialog-modal.png、issue37-real-confirm-modal.png)。

作者字段与编辑对话框紧凑化(2026-09-15,按用户要求)

按用户要求:①「章节标题」标签与输入框同行,下面新增「作者」标签与输入框;②精简插图/音频的提示文字,把高度让给正文编辑框。

  • schema v10:lexgo_chapters 增加 author VARCHAR(120) NOT NULL DEFAULT ''。由 Go 条件步骤 addChapterAuthorColumn(先查 information_schema 再 ALTER TABLE)在语句列表之后执行,保持「部分迁移可重试 / 回退标记后可重新升级」;新建库的 v3 语句也带该列。开发库迁移后 schema = 10,既有 34 个章节的 author 均为空串(无数据变化)。
  • 接口:章节编辑接受 author(可选、trim、≤120 字符、空串清空),ChapterSummary/ChapterSource 都返回它;仅改作者不会重新处理章节(状态保持不变)。
  • 界面:.field-row 把标签与输入框放同一行(标题、作者各一行);正文 textarea 提升到 18 行;插图与音频压成各一两行提示(规格写在下方一行);阅读页在标题下显示非空作者(data-testid="chapter-author-line")。
  • 验证:Go 89 项顶层用例(新增 TestMySQLChapterAuthorRoundTrip 与 TestMigrationFromV9AddsChapterAuthor,后者覆盖「旧库缺列 → 迁移补列 → 回退标记 → 重新升级」);学习端 157 单测与 26 项 E2E 通过;真实链路实测:标题标签与输入框中线偏差 <8px、作者行位于标题下方且标签同行、正文高度 415px(原约 250px)、保存作者后阅读页显示、清空后该行消失(截图 issue37-real-editor-layout.png、issue37-real-reader-author.png)。

书级作者与书籍页精简(2026-09-15,按用户要求)

按用户要求:①书籍页去掉「封面」「书籍封面」「JPG…4096×4096」「音频与插图按章节设置…」四段文字,把空间让给章节列表;②「编辑书名」改为**「编辑书籍」并可编辑书名与作者**;③书名右侧显示当前书籍作者。

  • schema v11:lexgo_books.author VARCHAR(120) NOT NULL DEFAULT '',复用 v10 的条件加列助手(改名 addAuthorColumn,表名取自本文件常量)。开发库迁移后 schema = 11,既有 25 本书的 author 均为空串;回退件 .local/lexgo-pre-issue37c.exe(回退写回版本号 10 即可)。
  • 接口:PATCH /api/v1/books/:id 接受可选 author(省略保留、空串清空、trim、≤120),BookSummary/BookRef 返回它;书籍编辑不再只改书名。
  • 界面:封面区块变成一行(预览 + 上传/替换 + 移除);.title-line 在书名右侧显示作者(未设置不显示);「编辑书籍」对话框用同行标签编辑书名与作者。
  • 验证:Go 91 项顶层用例(新增 TestMySQLBookAuthorRoundTrip 与 TestMigrationFromV10AddsBookAuthor);学习端 157 单测与 26 项 E2E 通过;真实链路实测封面行高 68px、章节列表紧随其后(间距 18px)、四段被删文字都不再出现、「编辑书籍」对话框含作者输入、保存后作者显示在书名右侧(同一行、位于书名之后)并在刷新后保留、清空后标签消失(截图 issue37-real-book-page.png)。

未验证

  • 真机与真实浏览器解码播放(媒体元素在测试中被桩替换)、真机听感与兼容性仍未验证(#21 起的既有缺口)。
  • Safari/Firefox 未测;渐进式流式播放仍未做(服务端 Range 已实现)。
  • 迁移会清空书级音频测试数据且回退到 v8 找不回;当前无真实用户,工单已记录。
  • 已有书籍/章节的存量内容不受影响,但没有做「把既有书级音频迁到章级」的转换(无真实数据)。

#32 修复与验证(2026-09-16)

根因与修复

编辑路径用 contentSHA("edit:<章节>:<内容摘要>") 派生任务键,与 lexgo_ingest_jobs.uq_job_request (owner_id, request_key) 相撞:把正文改回曾经用过的版本时派生出已存在的键,Create 触发 MySQL 1062,事务整体回滚并冒泡为通用 500。新增 stageEditJob:先按 (owner_id, chapter_id, content_sha256) 锁定查询该版本的任务行,找到就复用(重置为 pending、attempts=0、清空失败原因),找不到才用派生键创建。无 schema 变化,因此不需要迁移;回退件 .local/lexgo-pre-issue32.exe。

验证

项 结果
go vet ./... / gofmt -l 通过、无输出
LEXGO_TEST_DB_NAME=lexgo_test_issue13 python scripts/server.py test-integration 92 项顶层用例通过、0 跳过;新增 TestMySQLChapterEditBackToAPreviousVersion 覆盖 A→B→A→B(此前第三步 500)、「一章一版本一行任务」、被复用的行干净重启(pending、attempts=0、无 finished_at/error_reason)、失败版本再次成为当前版本时被复活、章节最终为 ready 且正文为最后一次编辑的内容、读者按该内容返回、正文未改动时不新增任务
真实 API+MySQL 21 项检查通过(.local/issue32-api-evidence.json):在开发实例上对一本 fixture 书籍执行 A→B→A→B,三步全部 200(此前第三步 500)、每步都 report versionChanged 并最终 ready、该章只有两条任务行且都以 ready 收尾、没有任务停在 pending、读者返回最后一次编辑的正文、章节内容摘要等于某个任务版本、再切回另一版本仍为 200 且仍是两行、删除 fixture 书籍后任务级联清空
治理 python -m unittest discover -s tests 65 项与 harness.py check --strict 通过

未验证

  • 只验证了「单章来回切换」;多章并发编辑、超大文本反复切换与移动端上的编辑体验未单独压测。
  • 与编辑器交互无关:本单只改后端编排,前端在 #10 已实现「正文改版会重新处理」的提示。

英汉词典的准备、导入与验证(#40)

# 1) 显式准备(需要网络,只在此步骤下载;产物不入库)
python scripts/dict_prepare.py
#    可用 --cache-dir .local/dictionaries/cache 复用已下载的源,--keep-going 在计数漂移时仍产出
#    打印条目数、三类音标计数、产物字节数与 sha256

# 2) 显式迁移(v12 增加 provider 并放开单行约束)
python scripts/server.py migrate

# 3) 导入:管理端「英语词典」页选择"英汉释义"后上传 .local/dictionaries/zh-dict-v1.zip
#    命令行等价:POST /api/v1/dictionaries/import(multipart: name/language/version/source/format + file)
#    导入失败或校验不通过时保留原有词典,不会替换成半成品。

# 4) 查询验证
curl -s http://127.0.0.1:8000/api/v1/dictionaries          # items[].provider 应为 wordnet / ecdict
python .local/verify-issue40-api.py                        # 28 项真实 API 检查
  • 资源路径:准备好的归档 .local/dictionaries/zh-dict-v1.zip(约 3.0 MB);WordNet 夹具沿用 .local/nlp-resources/wordnet.zip。测试在夹具缺失时跳过,不会静默通过。
  • 测试命令: LEXGO_TEST_DB_NAME=lexgo_test_issue40 python scripts/server.py test-integration(Go 96 项)、 cd learner && npx vitest --run(159 项)、npx playwright test(26 项,含 390×844 触摸视口)、 cd admin && npx pnpm test(32 项)。
  • 迁移回退:把 lexgo_schema.version 写回 11、删除 id=2 的词典行即可;旧二进制缺少 provider 字段的 读语句不受影响(新列有默认值),但旧二进制见到 v12 会拒绝启动并要求显式迁移,这是有意的保护。
  • 排错:provider 不存在 → 未执行 migrate;导入 400 且提示版本不符 → 归档不是当前 pin 生成的 (重新运行 dict_prepare.py);查词只有英英释义 → 英汉词典未导入或被停用。

验证登录有效期(#42)

# 固定值本身在测试里断言,改动会让测试立刻失败
cd server; go test ./app/lexgo/ -run TestMySQLSessionLifetimeIsThirtyDays -v
# 集成测试:到期前仍可用、到期后被拒绝(按 SessionLifetime 计算,不写死小时数)
LEXGO_TEST_DB_NAME=lexgo_test_issue42 python scripts/server.py test-integration
  • TestMySQLSessionLifetimeIsThirtyDays 校验三件事:常量等于 30 天、登录响应 expiresAt 与库内 expires_at 一致、库内只存在摘要(原始令牌查不到);TestMySQLAccountIsolationAndRevocation 在 SessionLifetime - 1h 仍成功、再前进 2 小时被拒。
  • 手工排查:SELECT owner_id, expires_at FROM lexgo_sessions ORDER BY expires_at DESC;。 若登录后立刻 401,先确认服务器时间与 expires_at 都以 UTC 存储(DATETIME 不存时区)。
  • 旧的 8 小时会话不受影响:各自按自己的 expires_at 自然到期,不需要数据迁移。