|
| 1 | +# [1009] 修复单个模板点击取消后重复点击会额外触发下载框的问题 |
| 2 | + |
| 3 | +## 1 相关文档 |
| 4 | +- [dddd.md](dddd.md) - 任务文档模板 |
| 5 | + |
| 6 | +## 2 任务相关的代码文件 |
| 7 | +- `src/Mogan/TemplateCenter/template_api.hpp` |
| 8 | +- `src/Mogan/TemplateCenter/template_api.cpp` |
| 9 | + |
| 10 | +## 3 如何测试 |
| 11 | + |
| 12 | +### 确定性测试(单元测试) |
| 13 | +```bash |
| 14 | +./bin/test_only template_api_test |
| 15 | +``` |
| 16 | + |
| 17 | +### 非确定性测试(功能验证) |
| 18 | + |
| 19 | +1. 启动 Mogan,进入 Template Center |
| 20 | +2. 点击一个**未缓存**的模板卡片 → 弹出下载进度对话框 |
| 21 | +3. 点击进度对话框的 **Cancel** 按钮 → 对话框应关闭 |
| 22 | +4. **立即再次点击同一个模板** → 应重新弹出下载进度对话框 |
| 23 | +5. 等待下载完成 → 模板应正常打开,**不应出现多个下载框** |
| 24 | +6. 重复步骤 2-5 多次(取消 3~5 次后再下载)→ 下载完成时应**只打开一次文档** |
| 25 | + |
| 26 | +### 3.2 边界场景 |
| 27 | + |
| 28 | +- 取消下载后快速切换到其他模板再切回 → 不应有残留下载影响新模板 |
| 29 | +- 取消下载后断网再点击 → 应正常提示下载失败,不出现残留状态 |
| 30 | + |
| 31 | +## 4 如何提交 |
| 32 | + |
| 33 | +提交前执行以下最少步骤: |
| 34 | + |
| 35 | +```bash |
| 36 | +# 编译验证 |
| 37 | +xmake build |
| 38 | +``` |
| 39 | + |
| 40 | +## 5 What |
| 41 | + |
| 42 | +修复模板下载过程中点击取消后,重新下载同一个模板会导致下载框和文档打开次数"每次递增"的 bug。 |
| 43 | + |
| 44 | +1. **`TemplateAPI` 新增 `abortDownload()` 私有方法** |
| 45 | + - 静默终止指定模板的网络请求,清理 `downloadReplies_` |
| 46 | + - **不**发射任何信号,仅用于内部清理 |
| 47 | + |
| 48 | +2. **`downloadTemplate()` 改调 `abortDownload()` 清理旧请求** |
| 49 | + - 启动新下载前,用 `abortDownload` 终止同一模板的旧请求 |
| 50 | + - 避免旧请求的信号干扰新下载流程 |
| 51 | + |
| 52 | +3. **`cancelDownload()` 清理后 emit `downloadFailed`** |
| 53 | + - 用户点击 Cancel 时,终止请求并发射 `downloadFailed(templateId, "Download cancelled")` |
| 54 | + - 确保 `TemplateManager::downloadTemplateSync()` 中的 `QEventLoop` 立即退出 |
| 55 | + |
| 56 | +4. **`onDownloadFinished()` 补充 HTTP status code 检查** |
| 57 | + - 原实现仅检查 `reply->error()`,当服务器返回 HTTP 404 但网络传输成功时,`QNetworkReply::error()` 可能仍为 `NoError` |
| 58 | + - 增加对 `HttpStatusCodeAttribute` 的检查,将 `>= 400` 的 HTTP 响应统一视为下载失败 |
| 59 | + |
| 60 | +## 6 Why |
| 61 | + |
| 62 | +### 6.1 现象 |
| 63 | + |
| 64 | +用户在下载模板时点击取消,再次点击同一模板重新下载,下载完成后: |
| 65 | +- 弹出多个(随取消次数递增)下载完成提示 |
| 66 | +- 同一文档被多次打开 |
| 67 | + |
| 68 | +### 6.2 根因 |
| 69 | + |
| 70 | +`TemplateAPI::cancelDownload()` 的实现顺序有问题: |
| 71 | + |
| 72 | +```cpp |
| 73 | +void TemplateAPI::cancelDownload(const QString& templateId) { |
| 74 | + auto it = downloadReplies_.find(templateId); |
| 75 | + if (it != downloadReplies_.end() && it.value()) { |
| 76 | + disconnect(it.value(), nullptr, this, nullptr); // 先断开信号 |
| 77 | + it.value()->abort(); // 再 abort |
| 78 | + it.value()->deleteLater(); |
| 79 | + downloadReplies_.erase(it); |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | +
|
| 84 | +- `disconnect` 后调用 `abort()`,`QNetworkReply::finished` 信号发出时,`onDownloadFinished` 已收不到 |
| 85 | +- `downloadFailed` 信号**永远不会被 emit** |
| 86 | +- `TemplateManager::downloadTemplateSync()` 中的 `QEventLoop` 收不到 `downloadCompleted` 或 `downloadFailed`,只能傻等 30 秒超时 |
| 87 | +
|
| 88 | +在这段超时期间: |
| 89 | +- 旧的调用 `downloadTemplateSync()` 的 UI 层对象仍然卡在 `loop.exec()` 中 |
| 90 | +- 旧对象仍然连接着 `TemplateManager` 的 `downloadCompleted` / `downloadFailed` 信号 |
| 91 | +- 用户点击"重新下载"后,新的下载完成时会 emit `downloadCompleted` |
| 92 | +- **新旧所有卡住的对象都会收到该信号**,导致模板被多次打开 |
| 93 | +
|
| 94 | +取消 N 次就会累积 N 个"幽灵对象",下次成功下载时同时触发 N+1 次完成处理。 |
| 95 | +
|
| 96 | +## 7 How |
| 97 | +
|
| 98 | +### 7.1 信号缺失问题 |
| 99 | +
|
| 100 | +`cancelDownload` 需要 emit `downloadFailed`,让 `downloadTemplateSync` 的 `QEventLoop` 立即退出。但 `downloadTemplate()` 内部在启动新下载前也会调用 `cancelDownload` 清理旧请求,此时 emit `downloadFailed` 会误伤新启动的下载。 |
| 101 | +
|
| 102 | +### 7.2 拆分为两个方法 |
| 103 | +
|
| 104 | +| 方法 | 触发场景 | 是否 emit 信号 | |
| 105 | +|------|----------|----------------| |
| 106 | +| `abortDownload` | `downloadTemplate` 内部启动新下载前 | 否 | |
| 107 | +| `cancelDownload` | 用户点击 Cancel 按钮 | 是,emit `downloadFailed` | |
| 108 | +
|
| 109 | +```cpp |
| 110 | +void TemplateAPI::abortDownload(const QString& templateId) { |
| 111 | + auto it = downloadReplies_.find(templateId); |
| 112 | + if (it != downloadReplies_.end() && it.value()) { |
| 113 | + disconnect(it.value(), nullptr, this, nullptr); |
| 114 | + it.value()->abort(); |
| 115 | + it.value()->deleteLater(); |
| 116 | + downloadReplies_.erase(it); |
| 117 | + } |
| 118 | +} |
| 119 | +
|
| 120 | +void TemplateAPI::cancelDownload(const QString& templateId) { |
| 121 | + auto it = downloadReplies_.find(templateId); |
| 122 | + if (it != downloadReplies_.end() && it.value()) { |
| 123 | + disconnect(it.value(), nullptr, this, nullptr); |
| 124 | + it.value()->abort(); |
| 125 | + it.value()->deleteLater(); |
| 126 | + downloadReplies_.erase(it); |
| 127 | + emit downloadFailed(templateId, tr("Download cancelled")); |
| 128 | + } |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +### 7.3 调用链路 |
| 133 | + |
| 134 | +**用户取消下载:** |
| 135 | +> 注:`QTMTemplateOpener` 是本文档中的概念化表述,对应实际代码中调用 |
| 136 | +> `TemplateManager::downloadTemplateSync()` 的 UI 层对象(如启动页的模板卡片打开逻辑)。 |
| 137 | +
|
| 138 | +``` |
| 139 | +QProgressDialog::canceled |
| 140 | + → UI 层对象::cancelDownload (templateId) |
| 141 | + → TemplateManager::cancelDownload (templateId) |
| 142 | + → TemplateAPI::cancelDownload (templateId) [emit downloadFailed] |
| 143 | + → TemplateManager::downloadFailed |
| 144 | + → downloadTemplateSync::QEventLoop::quit [立即退出] |
| 145 | +``` |
| 146 | + |
| 147 | +**重新下载(启动新请求前清理旧请求):** |
| 148 | +``` |
| 149 | +UI 层对象::openTemplate |
| 150 | + → TemplateManager::downloadTemplateSync |
| 151 | + → TemplateManager::downloadTemplate |
| 152 | + → TemplateAPI::downloadTemplate |
| 153 | + → TemplateAPI::abortDownload (templateId) [静默清理,不 emit] |
| 154 | +``` |
| 155 | + |
| 156 | +### 7.4 修复后行为 |
| 157 | + |
| 158 | +- 用户点击取消 → `cancelDownload` emit `downloadFailed` → `QEventLoop` 立刻退出 → 调用 `downloadTemplateSync()` 的 UI 层对象立即析构,不残留 |
| 159 | +- 重新下载同一个模板时,旧的对象已彻底销毁,不会"幽灵"接收新下载的完成信号 |
| 160 | +- `downloadTemplate` 内部调用 `abortDownload` 清理旧请求,不会误 emit `downloadFailed` 导致新下载被中断 |
0 commit comments