fix: 视频记录不再提前清空,启动重置残留的 running 状态 (#18)

真机使用时发现 55066525387 的子目录里有视频文件,界面上「目录」却不可点。
查明是取视频跑到一半程序被重启,videos 表里该商品的记录已被删除、
新记录还没写回,磁盘有文件而库里无记录。

缺陷一:prepareVideoFetch 在开始下载前先做了一次
ReplaceVideos(product.ID, nil, ...)。而 work.Finalize 里本来就有一次
ReplaceVideos(product.ID, records, now),ReplaceVideos 的语义就是按
product_id 全量替换。前面那次并不能少做任何事,唯一效果是把「文件已下载
但记录已被删」的窗口从零扩大到整个下载过程,任务停止、程序重启和崩溃
都会落进去。删掉它。

原注释说明作者考虑过这个风险,但只防住了登录失效一种情况。

行为变化是期望的:取视频在下载阶段之前失败时,旧的视频记录会保留而不是
被清空。那些文件确实还在磁盘上,「这次没找到视频」由 products.video_status
表达,不该靠删 videos 记录来表达。

缺陷二:进程退出后 download_status / upload_status 仍停留在 running,
但没有任何任务在跑,是个不会自愈的假状态。启动时在同一事务内把这两个字段
的 running 重置为 pending,只动 running,不碰 video_status 和 videos 表。
重置失败只记 Warn,不中断启动。

按负责人要求未对 cmsp.db 做任何手工订正。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
This commit is contained in:
QiuSW
2026-09-03 16:46:40 +08:00
co-authored by Claude Opus 5
parent 0fe24f70f4
commit 16e4f0faf8
3 changed files with 110 additions and 4 deletions
+33
View File
@@ -322,6 +322,39 @@ func (s *Store) UpdateProductStatus(id, videoStatus, downloadStatus, uploadStatu
return nil
}
// ResetRunningStatuses 把上次进程残留的运行中状态改回待处理。
func (s *Store) ResetRunningStatuses() (int, error) {
tx, err := s.db.Begin()
if err != nil {
return 0, fmt.Errorf("开启重置运行中状态事务失败:%w", err)
}
defer tx.Rollback()
downloadResult, err := tx.Exec(`UPDATE products SET download_status = ? WHERE download_status = ?`,
DownloadPending, DownloadRunning)
if err != nil {
return 0, fmt.Errorf("重置下载运行中状态失败:%w", err)
}
uploadResult, err := tx.Exec(`UPDATE products SET upload_status = ? WHERE upload_status = ?`,
UploadPending, UploadRunning)
if err != nil {
return 0, fmt.Errorf("重置上传运行中状态失败:%w", err)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("提交重置运行中状态事务失败:%w", err)
}
downloadCount, err := downloadResult.RowsAffected()
if err != nil {
return 0, fmt.Errorf("读取重置下载状态数量失败:%w", err)
}
uploadCount, err := uploadResult.RowsAffected()
if err != nil {
return 0, fmt.Errorf("读取重置上传状态数量失败:%w", err)
}
return int(downloadCount + uploadCount), nil
}
// CountResettableNoneProducts 返回当前被标记为「无同款视频」的商品数量。
func (s *Store) CountResettableNoneProducts() (int, error) {
var count int
+70
View File
@@ -6,6 +6,76 @@ import (
"testing"
)
func Test重置残留运行中状态只影响运行中字段(t *testing.T) {
s := newTestStore(t)
items := []Product{
{ID: "下载运行中", ItemID: "1", VideoDiagnosis: VideoDiagnosisMissing},
{ID: "上传运行中", ItemID: "2", VideoDiagnosis: VideoDiagnosisOK},
{ID: "两个都运行中", ItemID: "3", VideoDiagnosis: VideoDiagnosisMissing},
{ID: "已完成", ItemID: "4", VideoDiagnosis: VideoDiagnosisOK},
{ID: "已失败", ItemID: "5", VideoDiagnosis: VideoDiagnosisMissing},
{ID: "待处理", ItemID: "6", VideoDiagnosis: VideoDiagnosisOK},
{ID: "none状态", ItemID: "7", VideoDiagnosis: VideoDiagnosisMissing},
}
if err := s.UpsertProducts(items, "2026-09-03 10:00:00"); err != nil {
t.Fatalf("写入商品失败:%v", err)
}
statuses := map[string]struct{ download, upload string }{
"下载运行中": {DownloadRunning, UploadDone},
"上传运行中": {DownloadDone, UploadRunning},
"两个都运行中": {DownloadRunning, UploadRunning},
"已完成": {DownloadDone, UploadDone},
"已失败": {DownloadFailed, UploadFailed},
"待处理": {DownloadPending, UploadPending},
"none状态": {"none", "none"},
}
for id, status := range statuses {
if err := s.UpdateProductStatus(id, "", status.download, status.upload, ""); err != nil {
t.Fatalf("准备商品 %s 状态失败:%v", id, err)
}
}
if _, err := s.DB().Exec(`INSERT INTO videos (product_id, source_item, status) VALUES ('下载运行中', 'source', 'downloaded')`); err != nil {
t.Fatalf("准备视频记录失败:%v", err)
}
changed, err := s.ResetRunningStatuses()
if err != nil {
t.Fatalf("重置残留运行中状态失败:%v", err)
}
if changed != 4 {
t.Fatalf("应重置 4 个状态字段,实际 %d", changed)
}
want := map[string]struct {
download, upload, diagnosis string
}{
"下载运行中": {DownloadPending, UploadDone, VideoDiagnosisMissing},
"上传运行中": {DownloadDone, UploadPending, VideoDiagnosisOK},
"两个都运行中": {DownloadPending, UploadPending, VideoDiagnosisMissing},
"已完成": {DownloadDone, UploadDone, VideoDiagnosisOK},
"已失败": {DownloadFailed, UploadFailed, VideoDiagnosisMissing},
"待处理": {DownloadPending, UploadPending, VideoDiagnosisOK},
"none状态": {"none", "none", VideoDiagnosisMissing},
}
for id, expected := range want {
got, found, err := s.GetProduct(id)
if err != nil || !found {
t.Fatalf("读取商品 %s 失败:err=%v found=%v", id, err, found)
}
if got.DownloadStatus != expected.download || got.UploadStatus != expected.upload {
t.Fatalf("商品 %s 状态不正确:download=%q upload=%q", id, got.DownloadStatus, got.UploadStatus)
}
if got.VideoDiagnosis != expected.diagnosis || got.VideoStatus != VideoPending {
t.Fatalf("商品 %s 的视频状态被错误改动:diagnosis=%q videoStatus=%q", id, got.VideoDiagnosis, got.VideoStatus)
}
}
var videos int
if err := s.DB().QueryRow(`SELECT COUNT(*) FROM videos WHERE product_id = '下载运行中'`).Scan(&videos); err != nil || videos != 1 {
t.Fatalf("重置不得删除 videos 记录:count=%d err=%v", videos, err)
}
}
func Test老库升级保留商品和本地状态(t *testing.T) {
path := filepath.Join(t.TempDir(), "旧版.db")
db, err := sql.Open("sqlite", path)