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