Files
cmshoppe/docs/tasks/T-566.md
T
chengmaandClaude Opus 4.8 fa27134f7c docs(tasks): add T-566~T-569 cover gallery tasks (archive, gallery, regen, nav)
封面历史对比挑选功能拆四任务:T-566 数据层(重置归档改名、
update_generated_cover 状态语义、候选枚举排序、维持批次软删);
T-567 画廊查看+单选保存+原图弹窗;T-568 单任务重生+并发守卫
(含 fresh task 防跳过);T-569 上下条切换+未保存三态拦截。
含 Codex 评审修正与 title/cover committed 处理不对称的设计说明。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 11:13:21 +08:00

88 lines
6.9 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: TODO
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 请求链路。
## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)