--- id: T-693 title: 商品套图连续拖放丢失显示修复 phase: 7 deps: [T-692] status: DONE created: 2026-07-23 --- ## 问题 / 背景 T-692 已避免 Qt 默认内部投放复制同一图片项,但用户连续把参考1、参考2、参考3、参考4等图片拖到第一位时,商品原图列表仍可能突然看不到“参考1”,后续拖放也不能稳定写入排序。 现场 SQLite 检查确认相关项目的原图记录、状态和本地文件路径仍完整,未执行删除;异常属于 GUI 列表状态失配。当前 `ProductOriginalList._move_dragged_item_to()` 在 `dropEvent()` 尚未退出时同步发送 `orderChanged`,槽函数 `ProductSuiteTab.reorder_originals()` 随即写库并调用 `_refresh_originals()`,通过 `clear()` 删除全部旧 `QListWidgetItem` 后重新创建。此时 Qt 的 `QDrag` 仍持有本轮拖拽源和模型状态,连续拖动会让旧拖拽对象与新列表项发生重入,出现缩略图临时消失、标签缺位或后续排序未提交。 “主图 / 参考1 / 参考2”仍是当前位置标签,不是图片永久名称;每次排序后应连续重编号。只要原图不少于两张,界面刷新后必须存在“参考1”。 ## 方案 ### 1. 拖拽结束后再提交顺序 - `startDrag()` 开始前保存唯一的拖拽项和排序快照,并清空上一轮待提交状态。 - `dropEvent()` 只用 T-692 的受控 `takeItem()` + `insertItem()` 完成当前列表内移动;不在该事件内发送 `orderChanged`,也不触发列表刷新。 - 移动成功后仅保存待提交的唯一 asset ID 顺序;原地释放、无效目标、跨列表、锁定状态或重复 ID 不建立待提交顺序。 - 等 `super().startDrag()` 返回,确认 Qt 拖拽事务完全结束后,再清理拖拽上下文并发送一次 `orderChanged`。不使用固定毫秒延时猜测拖拽完成。 ### 2. 保存、失败恢复与显示一致性 - `ProductSuiteTab.reorder_originals()` 继续复用 `image_studio.reorder_original_assets()` 的 SQLite 单事务写入和唯一性校验;成功或失败后的完整刷新发生在拖拽事务之外。 - 写库成功后按数据库顺序重建列表,并连续显示“主图、参考1、参考2……”;保留勾选状态、图片 ID 和缩略图。 - 写库失败时重新读取数据库恢复原顺序并显示中文错误,不保留仅存在于 GUI 的临时顺序。 - 不删除、复制或新增原图资产,不修改本地文件;现有删除确认、生成/拉取锁定及历史 job 快照保持不变。 ### 3. 回归测试 - 扩展 `ProductOriginalList` Qt 测试,验证 `dropEvent()` 阶段只记录待提交顺序,不发送 `orderChanged`;拖拽结束后只发送一次。 - 连续模拟把不同参考图移到第一位,接收排序后重建列表,验证每轮图片数量不变、asset ID 唯一且集合不变,标签始终连续包含“主图、参考1……”;最终顺序与 SQLite 一致。 - 覆盖原地释放、重复 ID、无拖拽源和被锁定时不提交;外部文件 URL 拖入仍只走 `filesDropped`。 ## 验收要点 - 对同一商品连续把参考1、参考2、参考3、参考4拖到第一位,每次均只改变顺序,不丢失或复制任何缩略图。 - 原图不少于两张时,每轮刷新后都显示“参考1”,其余参考图按位置连续编号。 - 每次有效拖拽只写库一次;无效拖拽不写库、不显示成功。 - 切换商品或重启程序后,SQLite 中原图数量、图片 ID 集合和最终顺序与界面一致。 - 自动验证: - `py -3.10 -m unittest tests.test_product_suite_gui tests.test_image_studio tests.test_product_suite` - `py -3.10 -m unittest discover -s tests` - `py -3.10 -m ruff check app tests main.py` - `py -3.10 -m compileall app main.py` - `git diff --check` ## 边界(不改什么) - 不改 SQLite schema、图片文件、原图删除规则、生成任务、提示词、cmhub、蝦皮 CDP 或①至⑤模块。 - 不改变“主图 / 参考N”按当前位置动态编号的产品语义,不给图片增加永久参考编号。 - 不放宽服务层对重复 ID、缺失 ID 和跨项目 ID 的校验,不实现跨列表或多项拖放。 ## 执行记录 - 2026-07-23 完成。 - `ProductOriginalList` 新增单轮拖拽生命周期状态:开始时冻结原图 ID 集合,`dropEvent()` 仅以受控移动更新列表并保存待提交顺序,无效投放直接拒绝;`super().startDrag()` 返回后先清除旧拖拽项引用,再发送一次 `orderChanged`。因此页面写库和 `_refresh_originals()` 清空重建列表时,Qt 已不再持有上一轮拖拽源。 - 提交前同时校验当前列表、待提交顺序和拖拽开始快照的数量、唯一性与 ID 集合;原地释放、无拖拽源、重复 ID、列表中途变化和排序锁定均不提交。SQLite 服务层的完整集合与重复 ID 校验保持不变。 - 扩展列表级测试,确认移动发生时不会提前发信号,拖拽结束才发送一次;新增项目级连续换主图回归,依次把4张参考图移到第一位,每轮确认5张原图全部保留、标签连续、数据库在结束拖拽前不变且结束后与界面一致。同步更新 `docs/04-architecture.md` 与 `docs/routes.md`。 - 验证通过:`py -3.10 -m unittest tests.test_product_suite_gui tests.test_image_studio tests.test_product_suite`(109项)、`py -3.10 -m unittest discover -s tests`(676项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`。离屏 PySide6 仍有字体目录和插件能力告警,但无测试失败。