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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user