Files
cmshoppe/docs/tasks/T-566.md
T

103 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: T-566
title: 封面历史归档数据层:重置改名保留旧图 + update_generated_cover + 目录枚举
phase: 7
deps: [T-564]
status: DONE
created: 2026-07-09
---
## 问题 / 背景
运营需要对同一商品多次 AI 生图的结果做**对比挑选**(哪张更适合商品)。当前重置/重生封面时旧图被直接覆盖或删除(`reset_generated(delete_file=True)` 走 `os.remove`),历史生成图无法保留,也没有把 DB 封面指针改到某张历史图的手段。
设计定案(grill 评审收口):
- **不把多条路径塞进 `tasks.new_cover_path`**(一列多值反模式:所有读方按单路径消费、分隔符无安全选择、DB 与磁盘两个真相源必漂移)。`new_cover_path` 永远只存**当前生效的一张**。
- **目录即数据库**:历史候选图只存在文件系统,靠文件名约定 `{task_id}_{item_id}_new_<时间戳>.jpg` 索引,弹窗打开时现场扫目录枚举。
- 归档钩子放在**重置**(运营主动、非时间敏感),不放在生图写入热路径(避免在已扣点的成功路径上引入 Windows 文件锁/TOCTOU 新失败点)。
本任务是数据层前置;GUI 画廊弹窗见 T-567。
## 方案(改哪个文件、改成什么)
### `app/db.py` — `reset_generated` 归档改名(替代删除)
- `reset_generated(..., reset_cover=True)` 时,若 `new_cover_path` 指向的文件存在:
- 改名为 `{原名去扩展}_{YYYYMMDDHHMMSS}{ext}`,如 `42_51513423876_new.jpg` → `42_51513423876_new_20260709094800.jpg`;
- **撞名兜底**:目标名已存在(同秒重置两次)则追加 `_2`/`_3`;
- **顺序**:先 `os.rename` 成功、再清 DB 指针;改名抛 `PermissionError`(Windows 文件被预览/资源管理器占用)→ **整体中止本次重置**,抛 `DbError`(提示"请先关闭正在查看的封面图片再重置"),不留中间态(先清指针后改名失败会导致下次生成覆盖旧图、归档丢失);
- 旧文件不存在 → 无事发生,照常清指针(幂等,不报错)。
- `delete_file=True` 的旧语义处理:归档取代删除(或保留 `delete_file` 为真删、新增 `archive_previous` 参数,默认对封面重置开启——具体参数形状实现时定,验收看行为)。
- 仅重置标题(`reset_cover=False`)不触碰任何图片文件(既有行为不回归)。
### `app/db.py` — 新增 `update_generated_cover(task_id, new_cover_path)`
- 校验任务存在、目标文件存在,目标路径必须是本地文件路径;供 T-567「保存」调用,把 `tasks.new_cover_path` 指向运营选中的那张(不复制、不改名文件)。
- 状态语义必须明确:
- 写入后设置 `stage='generated'`、`status='pending'`、`last_error=NULL`、`updated_at=<now>`,表示本地已有可用于③更新蝦皮的新封面;
- 不增加 `generate_attempts` / `apply_attempts`,因为这不是一次新的 AI 调用或线上提交;
- 不改 `committed`、`applied_at`、`apply_attempts`,保留“曾经提交过线上”的历史事实;
- 不改 `image_task_id` / `image_task_key`,它们只记录/续查 cmhub 异步生图任务,封面指针切换不应误清审计信息;真正重置封面仍由 `reset_generated(..., reset_cover=True)` 清空。
- 对已 `committed=1` 或 `stage='applied'` 的任务,DB 层允许写入并按上面规则退回 `generated/pending`,由 GUI 在保存前弹“本地换封面不会回滚蝦皮,重复更新会再次提交线上”确认。
- **有意的不对称,勿"顺手对齐"**:`update_generated_title`(`db.py:819`)对 committed/applied 任务在 DB 层直接拒绝,而本函数允许(退回 + GUI 警示)。这是设计决定:换封面重挑是运营核心场景、走「重置更新状态」同款警示口径(`apply.py:311/334`);改标题重提交没有此需求。后续不要参照 title 的守卫给本函数加拒绝逻辑,否则 committed 记录重挑封面功能会被改死。
### 批次软删除边界:不做归档图片清理
- 不修改 `delete_batch()` 的软删除语义,不删除 `<image_root>/<batch_id>/` 目录。
- 理由:T-206 已定“删除批次”为软删除,只隐藏本地批次,不物理删除任务与图片;把本任务改成删目录会造成用户误删历史 AI 图和旧封面备份。
- 归档图片清理如后续需要,另做独立“清理已软删除批次图片”工具,必须有明确二次确认和删除清单预览。
### `app/image_paths.py` — 新增画廊枚举函数
- `list_task_cover_candidates(image_root, task, account=None)`:扫 `<image_root>/<batch_id>/<slug>/` 下匹配 `{task_id}_{item_id}_new*.jpg` 的文件,返回绝对路径列表;只包含新封面候选,不含 `_old` 旧封面。
- 排序规则必须稳定、可测试:
- 最新优先;
- 规范名 `{task_id}_{item_id}_new.jpg` 视为“当前最新输出槽”,排在同任务候选最前;
- 归档名 `{task_id}_{item_id}_new_YYYYMMDDHHMMSS.jpg` 按时间戳倒序;
- 同秒撞名后缀 `_2` / `_3` 按数字倒序;
- 无法解析时间戳的匹配文件排在最后,并按文件名升序兜底。
- GUI 默认选中由 DB 当前 `new_cover_path` 决定,不依赖排序;排序只影响画廊展示顺序。
- 只读、不建表、不加列——文件系统是唯一真相源。
## 验收要点
- 重置封面且旧图存在 → 旧图改名为带 `YYYYMMDDHHMMSS` 后缀、原规范名腾出、DB 指针已清;同秒二次重置 → 追加 `_2` 不覆盖。
- 改名 `PermissionError` → 本次重置整体失败、DB 指针未清、原文件原名保留(断言不留中间态)。
- 旧图不存在 → 重置照常成功(幂等)。
- 仅重置标题 → 不触碰图片文件。
- `update_generated_cover` 写入后 `stage=generated/status=pending/last_error=NULL`;不增加 attempts,不改 committed/applied_at/apply_attempts/image_task_*;目标文件不存在 → 报错。
- `delete_batch` 仍只做软删除,不删除 `<batch_id>/` 图片目录;归档图片继续保留。
- `list_task_cover_candidates` 返回全部 `_new*` 候选、不含 `_old`,排序符合“规范名优先、归档时间倒序、撞名数字倒序、异常文件名兜底”的规则。
- 验证命令(unittest,不引入 pytest):
- `py -3.10 -m unittest tests.test_db tests.test_image_paths`
- `python -m ruff check app tests main.py`
- `py -3.10 -m unittest discover -s tests`
- `git diff --check`
## 边界(不改什么)
- 不改 `tasks` 表 schema(不加列、不建子表);`new_cover_path` 仍为单路径。
- 不改生图写入链(`_save_jpeg`/`persist_cover_success`/规范名 `..._new.jpg`);归档只钩在重置。
- 不改 ③更新/Excel 回写对 `new_cover_path` 的消费方式。
- 不做 GUI(画廊弹窗在 T-567)。
- 不改 CDP/Shopee 交互、AI 请求链路。
## 执行记录
- 2026-07-09 完成 T-566。
- 修改 `app/db.py`:
- `reset_generated(..., reset_cover=True)` 在清空 DB 指针前先把当前 `new_cover_path` 指向的本地文件改名归档为 `{stem}_YYYYMMDDHHMMSS{ext}`;同秒撞名追加 `_2/_3`;旧 `delete_file=True` 不再物理删除,归档取代删除,返回结果新增 `archived_file`,保留 `deleted_file=None` 兼容旧调用。
- 文件不存在时幂等跳过归档;`PermissionError` 抛中文 `DbError` 并保持 DB 指针不变。
- 新增 `update_generated_cover(task_id, new_cover_path)`,只切换当前生效封面指针,写入后退回 `generated/pending`,不增加 attempts,不清 `committed/applied_at/apply_attempts/image_task_id/image_task_key`。
- 修改 `app/image_paths.py`:新增 `list_task_cover_candidates(image_root, task, account=None)`,只读扫描任务目录下 `_new*.jpg` 候选,返回绝对路径,排序为规范名优先、归档时间倒序、同秒后缀数字倒序、异常文件名最后。
- 修改测试:
- `tests/test_db.py` 覆盖归档撞名、文件锁失败 DB 不变、缺失文件幂等、`update_generated_cover` 状态语义、运行中拒绝切换、批次软删除不删图片目录。
- `tests/test_image_paths.py` 覆盖候选图过滤和稳定排序。
- `tests/test_gui.py` 同步重置后本地文件“归档保留而非原名保留”的新语义。
- 验证通过:
- `py -3.10 -m unittest tests.test_db tests.test_image_paths`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `py -3.10 -m unittest discover -s tests`(285 tests;PySide6 字体目录警告不影响结果)
- `git diff --check`