R4e:视频记录先删后写导致中断即丢,running 状态跨重启不恢复 #18

Open
opened 2026-09-03 16:23:27 +08:00 by ila · 2 comments
Owner

基本信息

  • 类型:缺陷
  • 所属 Epic:#2
  • 所属 MVP / 版本:#5
  • 阶段:待实施

依赖与并行

  • 前置工单:#15(R4c)、#17(R5)
  • 是否允许与前置工单并行:是
  • 原因:真实依赖是「前置工单的代码已合入 main」,不是「已通过人工验收」。
    #15 的 6cc334a、#17 的 d1149b6 与 0fe24f7 均已提交并推送到 origin/main,
    本工单在这些代码之上修改,依赖已满足。
    #15 / #17 的真机验收关注的是淘宝风控节奏与货憨憨上传链路,
    与本工单修改的「视频记录替换时机」和「启动重置 running 状态」互不重叠;
    若验收发现问题,会在对应工单另行处理,不会因此推翻本工单的改动。

子项目影响

  • 仅影响的子项目 / 交付单元:cmsp 桌面端
  • 是否跨子项目:否
  • 是否修改共享接口或契约:否
  • 各子项目需要执行的验证:见「验证方式」

原始需求

  • 来源:用户对话(真机使用时发现)
  • 提出时间:2026-09-03
  • 关键原话:「55066525387 子目录里有视频文件,无法点击目录打开。」

要解决什么

复现的真实故障

2026-09-03 16:14 对商品 55066525387(id 为 1124684165671051264)
重跑「取视频」,过程中程序被重启(为加载新的 Wails 绑定)。结果:

位置 实际状态
磁盘 运行数据/视频/55066525387/55066525387_1.mp4 存在,4514202 字节
videos 表 该商品一条记录都没有(原有记录被删,新记录没写)
products video_status='pending',download_status='running'
界面 「目录」链接不可点,看起来像从没下载过

缺陷一:先删后写,中断即丢记录

app.go 的 prepareVideoFetch 在开始下载之前先清空该商品的视频记录:

// 登录通过后才清理旧记录,避免一次登录失效把已下载记录抹掉。
if err := a.db.ReplaceVideos(product.ID, nil, time.Now().Format(...)); err != nil {

而 work.Finalize 在全部下载结束后本来就会再做一次完整替换:

if err := a.db.ReplaceVideos(product.ID, records, now); err != nil {

所以开头那次 ReplaceVideos(..., nil, ...) 是完全多余的——ReplaceVideos
本身就是「按 product_id 全量替换」,末尾那次已经覆盖了清理旧记录的需求。
它唯一的实际效果就是把记录提前抹掉,让中断窗口从「零」扩大到「整个下载过程」。

原注释说明作者考虑过这个风险,但只防住了「登录失效」这一种情况,
没有防住任务停止、程序重启和崩溃。

缺陷二:running 状态跨重启不会恢复

程序重启后 download_status='running' 仍然挂着,但没有任何任务在跑。
upload_status='running' 同理。这是个不会自愈的假状态:

  • 界面显示「下载中」,实际什么都没发生
  • 使用者无法判断该商品是否需要重跑

running 表示「此刻有任务正在处理」,进程一退出这个前提就不成立了。

做什么 / 不做什么

  • 做:
    • 删除 prepareVideoFetch 开头那次多余的 ReplaceVideos(..., nil, ...)
    • 启动时把 download_status='running' 与 upload_status='running' 重置为 pending
  • 不做:
    • 不做磁盘对账(扫描视频目录补回记录)——那是 #16 的范围
    • 不手动订正 55066525387 的任何状态(负责人明确要求不手动改),
      修复后由正常流程自行恢复
    • 不改取视频、图搜、下载、上传的业务逻辑
    • 不改 ReplaceVideos 本身的语义

已确认方案

缺陷一

删除 app.go 中 prepareVideoFetch 开头对 ReplaceVideos 的调用
(连同其注释),只保留 work.Finalize 里那一次。

在 Finalize 的那次调用处补注释,写明:

视频记录只在这里替换一次。不要在下载开始前先清空——ReplaceVideos
本身就是按 product_id 全量替换,提前清空并不能少做什么,只会让
「文件已下载但记录已被删」的窗口覆盖整个下载过程。任务中断、
程序重启或崩溃都会落进这个窗口,导致磁盘上有文件、库里没记录,
界面表现为「目录」不可点。

注意行为差异:删除后,如果一次取视频在下载阶段之前就失败
(搜同款失败、所有同款都没视频),旧的视频记录会保留下来而不是被清空。
这是期望行为——那些文件确实还在磁盘上。
「没找到视频」的语义由 products.video_status 承担,不由删记录来表达。

缺陷二

在 App 启动(startup / 数据库就绪后)调用一个新的
Store.ResetRunningStatuses() (int, error):

UPDATE products
   SET download_status = 'pending'
 WHERE download_status = 'running';
UPDATE products
   SET upload_status = 'pending'
 WHERE upload_status = 'running';
  • 两条语句在同一事务内执行
  • 返回受影响行数,通过 logx 输出「启动时重置了 N 个残留的运行中状态」
  • 只动 running,不碰 done / failed / pending / none
  • 不碰 video_status(它的取值里没有 running)
  • 不碰 videos 表

需求变化记录

日期 变化内容 原因 用户确认
2026-09-03 建立本工单 真机使用时发现磁盘有文件而库里无记录 是
2026-09-03 不手动订正 55066525387 的状态 负责人明确要求:「不要手动改 55066525387 的 upload_status」 是

设计与原型门禁

  • 修改类型:非 UI
  • 所需设计证据:无需原型(恢复既有预期行为的缺陷修复)
  • 无需 UI 原型的原因:不新增任何界面元素,只修正后端状态写入时机。
  • 状态:无

文档影响

  • 更新业务规则与术语:说明视频记录只在下载全部结束后替换一次,
    以及 running 状态的生命周期只在进程内有效、启动时会被重置。

交付文档影响

  • 无交付文档影响,原因:内部工具,无外部使用者文档。

任务记录与可选快照

  • 单次任务事实来源:当前 Gitea 工单正文与评论
  • 默认不创建任务快照

验收标准

  • prepareVideoFetch 中不再存在下载开始前的 ReplaceVideos(..., nil, ...)
  • 一次成功的取视频,结束后 videos 表内容与本次下载结果一致(全量替换生效)
  • 取视频在下载阶段之前失败时,旧的视频记录保留而不是被清空
  • 取视频在下载阶段中途被停止时,已有记录不会因此丢失
  • 启动时 download_status='running' 被重置为 pending
  • 启动时 upload_status='running' 被重置为 pending
  • 启动重置不改动 done / failed / pending / none 状态
  • 启动重置不改动 video_status 和 videos 表
  • 重置行数通过 logx 输出
  • 未新增 migration;未新增第三方依赖;internal/ 未 import wails

验证方式

go vet ./...
go build ./...
go test ./internal/... -v
go test ./...
cd frontend && npx vite build

单元测试需覆盖:

  • ResetRunningStatuses:造 running / done / failed / pending 各若干条,
    断言只有 running 被改成 pending,其余纹丝不动,返回行数正确
  • ResetRunningStatuses 不改动 video_status,不删 videos 记录
  • 取视频流程:模拟「搜同款阶段失败」,断言原有 videos 记录仍在
  • 取视频流程:模拟「下载阶段被取消」,断言原有 videos 记录仍在
  • 取视频流程:模拟成功,断言 videos 被本次结果全量替换

真机验证(负责人执行):

  1. 启动程序,观察运行日志中是否出现「启动时重置了 N 个残留的运行中状态」,
    且 55066525387 的 download_status 不再卡在「下载中」
  2. 对某个商品跑取视频,中途点「停止任务」,确认该商品原有的视频记录没有消失

风险和回退

  • 风险:删除前置清空后,取视频早期失败时会保留旧记录。若旧记录指向的文件
    已被手动删除,videos 表会留下指向不存在文件的记录。缓解:上传路径已有
    「取第一个 downloaded 且文件真实存在」的检查,缺文件场景已被兜住;
    彻底的磁盘对账在 #16 处理。
  • 回退:改动集中在 app.go 与 internal/store/product.go,git revert 即可。
    启动重置只把 running 改为 pending,回退代码不会撤销已重置的状态,
    但那些状态本来就是残留的假状态,保持 pending 是正确的。
## 基本信息 - 类型:缺陷 - 所属 Epic:#2 - 所属 MVP / 版本:#5 - 阶段:待实施 ## 依赖与并行 - 前置工单:#15(R4c)、#17(R5) - 是否允许与前置工单并行:**是** - 原因:真实依赖是「前置工单的代码已合入 main」,不是「已通过人工验收」。 #15 的 `6cc334a`、#17 的 `d1149b6` 与 `0fe24f7` 均已提交并推送到 `origin/main`, 本工单在这些代码之上修改,依赖已满足。 #15 / #17 的真机验收关注的是淘宝风控节奏与货憨憨上传链路, 与本工单修改的「视频记录替换时机」和「启动重置 running 状态」互不重叠; 若验收发现问题,会在对应工单另行处理,不会因此推翻本工单的改动。 ## 子项目影响 - 仅影响的子项目 / 交付单元:cmsp 桌面端 - 是否跨子项目:否 - 是否修改共享接口或契约:否 - 各子项目需要执行的验证:见「验证方式」 ## 原始需求 - 来源:用户对话(真机使用时发现) - 提出时间:2026-09-03 - 关键原话:「55066525387 子目录里有视频文件,无法点击目录打开。」 ## 要解决什么 ### 复现的真实故障 2026-09-03 16:14 对商品 `55066525387`(`id` 为 `1124684165671051264`) 重跑「取视频」,过程中程序被重启(为加载新的 Wails 绑定)。结果: | 位置 | 实际状态 | |---|---| | 磁盘 | `运行数据/视频/55066525387/55066525387_1.mp4` 存在,4514202 字节 | | `videos` 表 | 该商品**一条记录都没有**(原有记录被删,新记录没写) | | `products` | `video_status='pending'`,`download_status='running'` | | 界面 | 「目录」链接不可点,看起来像从没下载过 | ### 缺陷一:先删后写,中断即丢记录 `app.go` 的 `prepareVideoFetch` 在**开始下载之前**先清空该商品的视频记录: ```go // 登录通过后才清理旧记录,避免一次登录失效把已下载记录抹掉。 if err := a.db.ReplaceVideos(product.ID, nil, time.Now().Format(...)); err != nil { ``` 而 `work.Finalize` 在全部下载结束后**本来就会再做一次完整替换**: ```go if err := a.db.ReplaceVideos(product.ID, records, now); err != nil { ``` 所以开头那次 `ReplaceVideos(..., nil, ...)` **是完全多余的**——`ReplaceVideos` 本身就是「按 product_id 全量替换」,末尾那次已经覆盖了清理旧记录的需求。 它唯一的实际效果就是把记录提前抹掉,让中断窗口从「零」扩大到「整个下载过程」。 原注释说明作者考虑过这个风险,但只防住了「登录失效」这一种情况, 没有防住任务停止、程序重启和崩溃。 ### 缺陷二:running 状态跨重启不会恢复 程序重启后 `download_status='running'` 仍然挂着,但没有任何任务在跑。 `upload_status='running'` 同理。这是个不会自愈的假状态: - 界面显示「下载中」,实际什么都没发生 - 使用者无法判断该商品是否需要重跑 `running` 表示「此刻有任务正在处理」,进程一退出这个前提就不成立了。 ## 做什么 / 不做什么 - 做: - 删除 `prepareVideoFetch` 开头那次多余的 `ReplaceVideos(..., nil, ...)` - 启动时把 `download_status='running'` 与 `upload_status='running'` 重置为 `pending` - 不做: - **不做磁盘对账**(扫描视频目录补回记录)——那是 #16 的范围 - **不手动订正 `55066525387` 的任何状态**(负责人明确要求不手动改), 修复后由正常流程自行恢复 - 不改取视频、图搜、下载、上传的业务逻辑 - 不改 `ReplaceVideos` 本身的语义 ## 已确认方案 ### 缺陷一 删除 `app.go` 中 `prepareVideoFetch` 开头对 `ReplaceVideos` 的调用 (连同其注释),只保留 `work.Finalize` 里那一次。 在 `Finalize` 的那次调用处补注释,写明: > 视频记录只在这里替换一次。不要在下载开始前先清空——`ReplaceVideos` > 本身就是按 product_id 全量替换,提前清空并不能少做什么,只会让 > 「文件已下载但记录已被删」的窗口覆盖整个下载过程。任务中断、 > 程序重启或崩溃都会落进这个窗口,导致磁盘上有文件、库里没记录, > 界面表现为「目录」不可点。 **注意行为差异**:删除后,如果一次取视频在下载阶段之前就失败 (搜同款失败、所有同款都没视频),旧的视频记录会保留下来而不是被清空。 这是**期望行为**——那些文件确实还在磁盘上。 「没找到视频」的语义由 `products.video_status` 承担,不由删记录来表达。 ### 缺陷二 在 `App` 启动(`startup` / 数据库就绪后)调用一个新的 `Store.ResetRunningStatuses() (int, error)`: ```sql UPDATE products SET download_status = 'pending' WHERE download_status = 'running'; UPDATE products SET upload_status = 'pending' WHERE upload_status = 'running'; ``` - 两条语句在同一事务内执行 - 返回受影响行数,通过 logx 输出「启动时重置了 N 个残留的运行中状态」 - **只动 `running`**,不碰 `done` / `failed` / `pending` / `none` - **不碰 `video_status`**(它的取值里没有 `running`) - **不碰 `videos` 表** ## 需求变化记录 | 日期 | 变化内容 | 原因 | 用户确认 | |---|---|---|---| | 2026-09-03 | 建立本工单 | 真机使用时发现磁盘有文件而库里无记录 | 是 | | 2026-09-03 | 不手动订正 55066525387 的状态 | 负责人明确要求:「不要手动改 55066525387 的 upload_status」 | 是 | ## 设计与原型门禁 - 修改类型:非 UI - 所需设计证据:无需原型(恢复既有预期行为的缺陷修复) - 无需 UI 原型的原因:不新增任何界面元素,只修正后端状态写入时机。 - 状态:无 ## 文档影响 - [x] 更新业务规则与术语:说明视频记录只在下载全部结束后替换一次, 以及 `running` 状态的生命周期只在进程内有效、启动时会被重置。 ## 交付文档影响 - [x] 无交付文档影响,原因:内部工具,无外部使用者文档。 ## 任务记录与可选快照 - 单次任务事实来源:当前 Gitea 工单正文与评论 - [x] 默认不创建任务快照 ## 验收标准 - [ ] `prepareVideoFetch` 中不再存在下载开始前的 `ReplaceVideos(..., nil, ...)` - [ ] 一次成功的取视频,结束后 `videos` 表内容与本次下载结果一致(全量替换生效) - [ ] 取视频在**下载阶段之前**失败时,旧的视频记录**保留**而不是被清空 - [ ] 取视频在**下载阶段中途**被停止时,已有记录不会因此丢失 - [ ] 启动时 `download_status='running'` 被重置为 `pending` - [ ] 启动时 `upload_status='running'` 被重置为 `pending` - [ ] 启动重置**不改动** `done` / `failed` / `pending` / `none` 状态 - [ ] 启动重置**不改动** `video_status` 和 `videos` 表 - [ ] 重置行数通过 logx 输出 - [ ] 未新增 migration;未新增第三方依赖;`internal/` 未 import wails ## 验证方式 ``` go vet ./... go build ./... go test ./internal/... -v go test ./... cd frontend && npx vite build ``` 单元测试需覆盖: - `ResetRunningStatuses`:造 `running` / `done` / `failed` / `pending` 各若干条, 断言只有 `running` 被改成 `pending`,其余纹丝不动,返回行数正确 - `ResetRunningStatuses` 不改动 `video_status`,不删 `videos` 记录 - 取视频流程:模拟「搜同款阶段失败」,断言原有 `videos` 记录仍在 - 取视频流程:模拟「下载阶段被取消」,断言原有 `videos` 记录仍在 - 取视频流程:模拟成功,断言 `videos` 被本次结果全量替换 真机验证(负责人执行): 1. 启动程序,观察运行日志中是否出现「启动时重置了 N 个残留的运行中状态」, 且 `55066525387` 的 `download_status` 不再卡在「下载中」 2. 对某个商品跑取视频,中途点「停止任务」,确认该商品原有的视频记录没有消失 ## 风险和回退 - **风险**:删除前置清空后,取视频早期失败时会保留旧记录。若旧记录指向的文件 已被手动删除,`videos` 表会留下指向不存在文件的记录。缓解:上传路径已有 「取第一个 `downloaded` 且文件真实存在」的检查,缺文件场景已被兜住; 彻底的磁盘对账在 #16 处理。 - **回退**:改动集中在 `app.go` 与 `internal/store/product.go`,`git revert` 即可。 启动重置只把 `running` 改为 `pending`,回退代码不会撤销已重置的状态, 但那些状态本来就是残留的假状态,保持 `pending` 是正确的。
Author
Owner

依赖声明更正(2026-09-03)

实施 Agent 依据 AGENTS.md「开始实施前检查工单声明的前置工单,真实依赖未满足时保持待实施」拒绝开工,判断正确——是本工单的依赖声明写错了。

原声明「不允许与前置工单并行」,而 #15 / #17 处于待验收,于是形成死锁。

实际的依赖是代码已合入 main,不是已通过人工验收:

  • #15 → 6cc334a
  • #17 → d1149b6、0fe24f7

三个提交均已推送到 origin/main,本工单在其之上修改,依赖已满足。
且 #15 / #17 的真机验收关注淘宝风控节奏与货憨憨上传链路,
与本工单的「视频记录替换时机」「启动重置 running 状态」互不重叠。

正文「依赖与并行」一节已更正为允许并行并写明原因,重新派发实施。

### 依赖声明更正(2026-09-03) 实施 Agent 依据 AGENTS.md「开始实施前检查工单声明的前置工单,真实依赖未满足时保持待实施」拒绝开工,**判断正确**——是本工单的依赖声明写错了。 原声明「不允许与前置工单并行」,而 #15 / #17 处于待验收,于是形成死锁。 实际的依赖是**代码已合入 main**,不是**已通过人工验收**: - #15 → `6cc334a` - #17 → `d1149b6`、`0fe24f7` 三个提交均已推送到 `origin/main`,本工单在其之上修改,依赖已满足。 且 #15 / #17 的真机验收关注淘宝风控节奏与货憨憨上传链路, 与本工单的「视频记录替换时机」「启动重置 running 状态」互不重叠。 正文「依赖与并行」一节已更正为**允许并行**并写明原因,重新派发实施。
Author
Owner

最终实施证据

提交:16e4f0f fix: 视频记录不再提前清空,启动重置残留的 running 状态 (#18)
已推送到 origin/main。

改动文件(3 个):app.go、internal/store/product.go、product_test.go。

  • 删除 prepareVideoFetch 中下载开始前的 ReplaceVideos(product.ID, nil, ...),
    只保留 work.Finalize 里的全量替换,并补注释说明为什么不能提前清空
  • 新增 Store.ResetRunningStatuses(),在同一事务内把
    download_status / upload_status 的 running 重置为 pending
  • 在 app.go 数据库就绪后调用,失败只记 Warn 不中断启动,行数为 0 时不输出日志

验证结果:go vet ./...、go build ./...、go test ./...、
npx vite build 全部通过。

9 项硬性验收逐条复核全部通过:前置清空已删且 Finalize 保留;
ResetRunningStatuses 只动 running,不碰 video_status 和 videos 表;
未新增 migration;internal/ 未 import wails;未改前端与
internal/store/store.go、video.go;未对 cmsp.db 做任何手工订正
(日志中所有 UPDATE 均为源码行或 t.TempDir() 测试库);
未新增依赖;未执行 git 或 Gitea API;未改动 config.yaml、payloads/。

缺陷二已在真实数据上自证:Codex 修改 app.go 后,
负责人正在运行的 wails dev 自动重编译重启,启动时执行了新增的
ResetRunningStatuses,把 55066525387 卡住的 download_status='running'
重置为 pending。这不是手工订正,是修复本身生效。

未覆盖测试:「取视频早期失败保留旧记录」与「下载中途取消保留旧记录」
两个行为没有集成测试。现有结构不便注入,而注入需改动
internal/task/ 或 internal/taobao/,超出本工单范围,实施方按要求停手并报告。

待负责人真机验证

  1. 启动程序,观察日志是否出现「启动时重置了 N 个残留的运行中状态」
  2. 对某个商品跑取视频,中途点「停止任务」,
    确认该商品原有的视频记录没有消失

文档影响:声明需更新业务规则与术语。尚未执行,待验收后统一处理。

工单保持「待验收」。

### 最终实施证据 **提交**:`16e4f0f` fix: 视频记录不再提前清空,启动重置残留的 running 状态 (#18) 已推送到 `origin/main`。 **改动文件**(3 个):`app.go`、`internal/store/product.go`、`product_test.go`。 - 删除 `prepareVideoFetch` 中下载开始前的 `ReplaceVideos(product.ID, nil, ...)`, 只保留 `work.Finalize` 里的全量替换,并补注释说明为什么不能提前清空 - 新增 `Store.ResetRunningStatuses()`,在同一事务内把 `download_status` / `upload_status` 的 `running` 重置为 `pending` - 在 `app.go` 数据库就绪后调用,失败只记 Warn 不中断启动,行数为 0 时不输出日志 **验证结果**:`go vet ./...`、`go build ./...`、`go test ./...`、 `npx vite build` 全部通过。 9 项硬性验收逐条复核全部通过:前置清空已删且 `Finalize` 保留; `ResetRunningStatuses` 只动 `running`,不碰 `video_status` 和 `videos` 表; 未新增 migration;`internal/` 未 import wails;未改前端与 `internal/store/store.go`、`video.go`;**未对 `cmsp.db` 做任何手工订正** (日志中所有 UPDATE 均为源码行或 `t.TempDir()` 测试库); 未新增依赖;未执行 git 或 Gitea API;未改动 `config.yaml`、`payloads/`。 **缺陷二已在真实数据上自证**:Codex 修改 `app.go` 后, 负责人正在运行的 `wails dev` 自动重编译重启,启动时执行了新增的 `ResetRunningStatuses`,把 `55066525387` 卡住的 `download_status='running'` 重置为 `pending`。这不是手工订正,是修复本身生效。 **未覆盖测试**:「取视频早期失败保留旧记录」与「下载中途取消保留旧记录」 两个行为没有集成测试。现有结构不便注入,而注入需改动 `internal/task/` 或 `internal/taobao/`,超出本工单范围,实施方按要求停手并报告。 **待负责人真机验证** 1. 启动程序,观察日志是否出现「启动时重置了 N 个残留的运行中状态」 2. 对某个商品跑取视频,中途点「停止任务」, 确认该商品原有的视频记录**没有消失** **文档影响**:声明需更新业务规则与术语。**尚未执行**,待验收后统一处理。 工单保持「待验收」。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: chengma/cmsp#18