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

67 lines
5.7 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-694
title: 商品套图拖放源项二次删除修复
phase: 7
deps: [T-693]
status: DONE
created: 2026-07-23
---
## 问题 / 背景
T-693 将排序信号延后到 `super().startDrag(Qt.MoveAction)` 返回后,解决了拖拽期间同步刷新列表的问题,但真实 GUI 连续把参考图拖到第一位时,仍会按照拖动顺序逐张从商品原图区域消失,最终只剩下未被拖动的主图。
现场 SQLite 再次确认原图记录、状态和本地文件均完整,列表消失没有写入数据库。真实根因是当前代码同时使用两套移动所有权:自定义 `dropEvent()` 已通过 `takeItem()` + `insertItem()` 手动移动源项,而 `QAbstractItemView.startDrag()` 在 `MoveAction` 完成后还会按 Qt 内建 model/view 语义清理移动源项。一次拖拽因此发生“手动移动一次 + Qt 源端删除一次”,每轮少一个 GUI 项。
T-693 的测试直接调用 `_begin_internal_drag()`、`_move_dragged_item_to()`、`_finish_internal_drag()`,绕过了真实的 `super().startDrag()`,所以没有覆盖 Qt 返回 MoveAction 后的源端清理,形成了错误的全绿结果。
## 方案
### 1. 单一拖拽所有权
- `ProductOriginalList.startDrag()` 不再调用 `super().startDrag(Qt.MoveAction)`。
- 由列表自行创建 `QDrag`,source 固定为当前列表;MIME 只承载当前单张原图的内部拖拽数据,拖拽缩略图作为可选视觉反馈,并以 `Qt.MoveAction` 执行。
- 自定义 `dropEvent()` 继续是唯一改变列表顺序的位置,只调用一次受控 `takeItem()` + `insertItem()`;不交给 Qt 默认 model drop,也不允许 Qt 再删除移动源项。
- `QDrag.exec()` 返回后清理拖拽上下文,再按 T-693 的快照规则提交一次唯一顺序。
### 2. 失败自动恢复
- 拖拽开始快照、待提交顺序和结束时列表必须数量相等、ID 唯一且集合一致。
- 若拖拽结束时列表数量或 ID 集合异常,不写库;发送独立恢复信号,让 `ProductSuiteTab` 从 SQLite 重新加载当前商品原图,并显示“原图排序未完成,已恢复原顺序,请重试”。
- 恢复信号不得复用 `orderChanged`,避免把残缺或旧顺序误写数据库;切换任务、数据库异常或页面已关闭时保持无破坏失败。
- SQLite `reorder_original_assets()` 的完整集合、重复 ID 和项目归属校验继续作为服务层最后防线。
### 3. 覆盖真实 startDrag 路径
- 测试必须调用 `ProductOriginalList.startDrag()`,用可控 `QDrag` 替身在 `exec()` 中触发一次内部移动;验证没有调用基类 `startDrag()` 的二次删除语义,结束后图片数量和 ID 集合不变且只发送一次 `orderChanged`。
- 连续把参考1、参考2、参考3、参考4移到第一位,每轮都经过完整自定义 `startDrag()` 生命周期,并验证 SQLite 顺序、缩略图数量和“主图 / 参考N”连续标签。
- 模拟 `QDrag.exec()` 返回前列表被意外移除一项,验证不发送 `orderChanged`、发送一次恢复信号,页面从 SQLite 恢复全部原图。
- 保留原地释放、取消拖拽、重复 ID、排序锁定和外部文件 URL 拖入回归。
## 验收要点
- 连续把多张参考图拖到第一位,商品原图数量始终不变,不再按拖动顺序消失。
- 每次有效拖拽只移动一个现有项,不复制、不删除;结束后 GUI 与 SQLite 的图片 ID 集合及顺序一致。
- 拖拽过程中若出现任何数量或 ID 异常,界面自动恢复数据库顺序,不把异常状态保存。
- 原图不少于两张时始终存在“参考1”,所有参考图按当前位置连续编号。
- 自动验证:
- `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 或①至⑤模块。
- 不改外部文件拖入和剪贴板导入;内部 MIME 只用于当前列表单张图片排序,不接受跨列表或跨商品拖放。
- 不以 `CopyAction` 伪装移动来规避 Qt 清理,不使用固定延时,不放宽服务层数据完整性校验。
## 执行记录
- 2026-07-23 完成。
- `ProductOriginalList.startDrag()` 不再调用 `QAbstractItemView.startDrag(MoveAction)`,改为创建 source 为当前列表的 `QDrag`,使用私有 MIME 类型承载单张原图 ID,并复用缩略图作为拖拽视觉反馈。自定义 `dropEvent()` 仍是唯一执行 `takeItem()` + `insertItem()` 的位置,因此不会再由 Qt 基类二次删除移动源项。
- 拖拽进入、移动和投放同时校验当前列表来源及私有 MIME;外部文件 URL 仍走原导入路径。`QDrag.exec()` 返回后才提交待保存顺序,取消或结束时数量、唯一性、ID 集合异常均不写库,并通过独立 `orderRestoreRequested` 信号让页面从 SQLite 恢复原图及显示中文重试提示。
- 测试改为模拟完整 `startDrag()` 路径并控制 `QDrag.exec()`:覆盖单次移动不触发源端删除、连续4次更换主图后5张图片和连续标签保持完整、异常少项不发送排序信号,以及页面从 SQLite 自动恢复。同步更新 `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`(112项)、`py -3.10 -m unittest discover -s tests`(679项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`。离屏 PySide6 仍有字体目录和插件能力告警,但无测试失败。