8.5 KiB
8.5 KiB
id, title, phase, deps, status, created
| id | title | phase | deps | status | created | |
|---|---|---|---|---|---|---|
| T-566 | 封面历史归档数据层:重置改名保留旧图 + update_generated_cover + 目录枚举 | 7 |
|
DONE | 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_pathspython -m ruff check app tests main.pypy -3.10 -m unittest discover -s testsgit 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_pathspython -m ruff check app tests main.pypy -3.10 -m compileall app main.pypy -3.10 -m unittest discover -s tests(285 tests;PySide6 字体目录警告不影响结果)git diff --check