Table of Contents
- 本地开发与验证
- 环境要求
- Windows PowerShell 与 UTF-8
- 第一次运行
- 常用调试方式
- 完成修改前
- 可复制 Shell 示例
- 学习端工程创建后的验证方向
- 工程基础运行与验证(#2)
- 本机 supervisor 启动实例(2026-09-10)
- 本机验收账号维护(2026-09-10)
- 审计模块启动与验证(#18 / schema v2)
- #3 英语离线验证入口与复现
- #4 阅读选择验证
- #5 粘贴导入与章节阅读(schema v3)
- #5 审核整改验证(R1~R4,2026-09-11)
- #6 部署与验证(2026-09-11)
- #7 验证与迁移(2026-09-11)
- #8 验证与迁移(2026-09-11)
- #8 审核整改(R1~R3,2026-09-11)
- #9 验证与迁移(2026-09-11)
- #10 验证与迁移(2026-09-11)
- #11 验证与迁移(2026-09-11)
- #12 验证与迁移(2026-09-11)
- #13 迁移与验证(2026-09-15)
- #14 移动与主题验证(2026-09-15)
- #15 交付、备份与恢复验证(2026-09-15)
- 运维命令
- 完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])
- 性能观察(scripts/bench.py,人造数据集,单机无并发)
- 集成验证矩阵(复用既有 Go 测试,LEXGO_TEST_DB_NAME=lexgo_test_issue13 70 项全过)
- 治理与其它测试
- 未验证
- #15 交付、备份与恢复验证(2026-09-15)
- 运维命令
- 两条通道的交叉验证(2026-09-15,19 项检查全部通过)
- 完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])
- 性能观察(scripts/bench.py,人造数据集,单机无并发)
- 集成验证矩阵(复用既有 Go 测试,LEXGO_TEST_DB_NAME=lexgo_test_issue13 70 项全过)
- 治理与其它测试
- 未验证
- #15 交付、备份与恢复验证(2026-09-15)
- 运维命令
- 纯二进制安装演练(2026-09-15,9 项检查全部通过)
- 两条通道的交叉验证(2026-09-15,19 项检查全部通过)
- 完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])
- 性能观察(scripts/bench.py,人造数据集,单机无并发)
- 集成验证矩阵(复用既有 Go 测试,LEXGO_TEST_DB_NAME=lexgo_test_issue13 70 项全过)
- 治理与其它测试
- 未验证
- #21 附件功能验证(2026-09-15)
- #37 章级附件验证(2026-09-15)
- 迁移
- 真实链路(52 项)
- 恢复演练(第三次,22 项)
- 插图交互修订(2026-09-15,按用户要求)
- 插图交互修订(2026-09-15,按用户要求)
- 附件并入章节编辑对话框(2026-09-15,按用户要求)
- 对话框浮层缺陷修复(2026-09-15,用户发现)
- 作者字段与编辑对话框紧凑化(2026-09-15,按用户要求)
- 书级作者与书籍页精简(2026-09-15,按用户要求)
- 验收后的最终状态(2026-09-16)
- 未验证
- #37 章级附件验证(2026-09-15)
- #37 章级附件验证(2026-09-15)
- 迁移
- 真实链路(52 项)
- 恢复演练(第三次,22 项)
- 插图交互修订(2026-09-15,按用户要求)
- 插图交互修订(2026-09-15,按用户要求)
- 附件并入章节编辑对话框(2026-09-15,按用户要求)
- 未验证
- #37 章级附件验证(2026-09-15)
- 迁移
- 真实链路(52 项)
- 恢复演练(第三次,22 项)
- 插图交互修订(2026-09-15,按用户要求)
- 插图交互修订(2026-09-15,按用户要求)
- 附件并入章节编辑对话框(2026-09-15,按用户要求)
- 对话框浮层缺陷修复(2026-09-15,用户发现)
- 未验证
- #37 章级附件验证(2026-09-15)
- 迁移
- 真实链路(52 项)
- 恢复演练(第三次,22 项)
- 插图交互修订(2026-09-15,按用户要求)
- 插图交互修订(2026-09-15,按用户要求)
- 附件并入章节编辑对话框(2026-09-15,按用户要求)
- 对话框浮层缺陷修复(2026-09-15,用户发现)
- 作者字段与编辑对话框紧凑化(2026-09-15,按用户要求)
- 未验证
- #37 章级附件验证(2026-09-15)
- 迁移
- 真实链路(52 项)
- 恢复演练(第三次,22 项)
- 插图交互修订(2026-09-15,按用户要求)
- 插图交互修订(2026-09-15,按用户要求)
- 附件并入章节编辑对话框(2026-09-15,按用户要求)
- 对话框浮层缺陷修复(2026-09-15,用户发现)
- 作者字段与编辑对话框紧凑化(2026-09-15,按用户要求)
- 书级作者与书籍页精简(2026-09-15,按用户要求)
- 未验证
- #32 修复与验证(2026-09-16)
- 英汉词典的准备、导入与验证(#40)
- 验证登录有效期(#42)
本地开发与验证
环境要求
当前工作区为 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 由开发代理转发到后端,不开启宽泛跨域。
- 在 MySQL 8 中准备独立空库 lexgo_dev 与 lexgo_test_issue2(utf8mb4);不要把既有业务库用作测试库。
- 复制根 .env.example 为 .env.local,填写本机 DB 凭据与初始管理员密码;.env.local 已被 Git 忽略。优先读取进程中已有 LEXGO_* 配置,文件仅补缺项。
- 在仓库根执行以下后端命令。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,一次性工具)依次执行:
- 干净安装:
init-database建空库 →migrate(schema v7,14 张表)→ 确认「新库没有账号、没有业务数据」→bootstrap建管理员 → 再次bootstrap被拒绝(退出码 1,不覆盖管理员)→ 起独立 API 实例(:8010)。 - 两个演练账号走通闭环(
ops.py smoke,19 项全过):管理员建trial_a/trial_b→ 新账号书库与进度为空 → 粘贴章节 → 章节处理完成 → 分词返回可点选词 → 词典查询返回明确状态 → 保存词义 → 另一账号查不到该词条 → 新词立刻到期 → 作答成功 → 重复作答不重复记账 → 完成章节只记已读 → 进度反映活动;B读A的书 404、书库仍为空。 - 备份 lexgo_dev(约 12 MB gz):manifest 记录 schema 7、提交哈希、逐表行数、sha256;确认 manifest 无凭据字段;确认 dump 不含
CREATE DATABASE/USE(不会误写回源库)。 - 恢复到空库
lexgo_restore_drill并自动校验(33 项全过):逐表行数与备份一致(sys_user6、books27、chapters33、terms13、term_reviews13、review_answers55、login_logs192 等)→ 逐表内容校验和与源库一致(13 张表)→ 源库 lexgo_dev 未被改动。 - 安全边界:恢复默认拒绝覆盖已有库;拒绝写入系统库
mysql;不带--confirm直接拒绝。 - 恢复实例的两账号闭环与越权校验(
:8011,41 项全过)——恢复后的账号仍可登录、词条与到期队列仍在、答题与完成章节保持幂等、B无法读取或修改A的数据。 - 收尾:删除两个演练库,确认
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 工具各做一次备份,并互相恢复对方的备份:
- 两条通道的
manifest.json字段齐备、逐表行数一致、schema_version一致、都不含凭据字段; - Go 产出的 dump 由 Python 恢复成功,Python 产出的 dump 由 Go 恢复成功(
lexgo restore输出verified); - Go 校验 Go 恢复出来的库、Python 校验 Python 恢复出来的库,逐表行数与 manifest 一致;
- 两个恢复实例的逐表内容校验和都与源库一致(13 张表),源库在过程中未被改动;
lexgo verify可以直接校验源库;- 安全边界两条通道各自确认:Go 拒绝覆盖已有库、要求
--confirm,Python 拒绝系统库。
证据:.local/issue15-crosscheck-evidence.json。Go 侧另有 TestMySQLOpsBackupRestoreRoundTrip 集成用例(备份 → 恢复到空库 → 校验 → 拒绝二次覆盖 → 源库校验和不变)。
完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])
演练脚本(.local/tmp/issue15-drill.py,一次性工具)依次执行:
- 干净安装:
init-database建空库 →migrate(schema v7,14 张表)→ 确认「新库没有账号、没有业务数据」→bootstrap建管理员 → 再次bootstrap被拒绝(退出码 1,不覆盖管理员)→ 起独立 API 实例(:8010)。 - 两个演练账号走通闭环(
ops.py smoke,19 项全过):管理员建trial_a/trial_b→ 新账号书库与进度为空 → 粘贴章节 → 章节处理完成 → 分词返回可点选词 → 词典查询返回明确状态 → 保存词义 → 另一账号查不到该词条 → 新词立刻到期 → 作答成功 → 重复作答不重复记账 → 完成章节只记已读 → 进度反映活动;B读A的书 404、书库仍为空。 - 备份 lexgo_dev(约 12 MB gz):manifest 记录 schema 7、提交哈希、逐表行数、sha256;确认 manifest 无凭据字段;确认 dump 不含
CREATE DATABASE/USE(不会误写回源库)。 - 恢复到空库
lexgo_restore_drill并自动校验(33 项全过):逐表行数与备份一致(sys_user6、books27、chapters33、terms13、term_reviews13、review_answers55、login_logs192 等)→ 逐表内容校验和与源库一致(13 张表)→ 源库 lexgo_dev 未被改动。 - 安全边界:恢复默认拒绝覆盖已有库;拒绝写入系统库
mysql;不带--confirm直接拒绝。 - 恢复实例的两账号闭环与越权校验(
:8011,41 项全过)——恢复后的账号仍可登录、词条与到期队列仍在、答题与完成章节保持幂等、B无法读取或修改A的数据。 - 收尾:删除两个演练库,确认
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 工具各做一次备份,并互相恢复对方的备份:
- 两条通道的
manifest.json字段齐备、逐表行数一致、schema_version一致、都不含凭据字段; - Go 产出的 dump 由 Python 恢复成功,Python 产出的 dump 由 Go 恢复成功(
lexgo restore输出verified); - Go 校验 Go 恢复出来的库、Python 校验 Python 恢复出来的库,逐表行数与 manifest 一致;
- 两个恢复实例的逐表内容校验和都与源库一致(13 张表),源库在过程中未被改动;
lexgo verify可以直接校验源库;- 安全边界两条通道各自确认:Go 拒绝覆盖已有库、要求
--confirm,Python 拒绝系统库。
证据:.local/issue15-crosscheck-evidence.json。Go 侧另有 TestMySQLOpsBackupRestoreRoundTrip 集成用例(备份 → 恢复到空库 → 校验 → 拒绝二次覆盖 → 源库校验和不变)。
完整演练(2026-09-15,本机一次性通过,27 项顶层检查全部 [ok])
演练脚本(.local/tmp/issue15-drill.py,一次性工具)依次执行:
- 干净安装:
init-database建空库 →migrate(schema v7,14 张表)→ 确认「新库没有账号、没有业务数据」→bootstrap建管理员 → 再次bootstrap被拒绝(退出码 1,不覆盖管理员)→ 起独立 API 实例(:8010)。 - 两个演练账号走通闭环(
ops.py smoke,19 项全过):管理员建trial_a/trial_b→ 新账号书库与进度为空 → 粘贴章节 → 章节处理完成 → 分词返回可点选词 → 词典查询返回明确状态 → 保存词义 → 另一账号查不到该词条 → 新词立刻到期 → 作答成功 → 重复作答不重复记账 → 完成章节只记已读 → 进度反映活动;B读A的书 404、书库仍为空。 - 备份 lexgo_dev(约 12 MB gz):manifest 记录 schema 7、提交哈希、逐表行数、sha256;确认 manifest 无凭据字段;确认 dump 不含
CREATE DATABASE/USE(不会误写回源库)。 - 恢复到空库
lexgo_restore_drill并自动校验(33 项全过):逐表行数与备份一致(sys_user6、books27、chapters33、terms13、term_reviews13、review_answers55、login_logs192 等)→ 逐表内容校验和与源库一致(13 张表)→ 源库 lexgo_dev 未被改动。 - 安全边界:恢复默认拒绝覆盖已有库;拒绝写入系统库
mysql;不带--confirm直接拒绝。 - 恢复实例的两账号闭环与越权校验(
:8011,41 项全过)——恢复后的账号仍可登录、词条与到期队列仍在、答题与完成章节保持幂等、B无法读取或修改A的数据。 - 收尾:删除两个演练库,确认
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自然到期,不需要数据迁移。