From 48ee76d60dc1a62d75400bf4352dcf387c09ab4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Thu, 10 Sep 2026 15:26:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(patrol-delete):=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E8=B7=91=E5=AE=8C=E4=B8=8D=E5=87=BA=E7=8E=B0=E3=80=8C=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E7=BB=93=E6=9E=9C=E3=80=8D=E6=8C=89=E9=92=AE=E2=80=94?= =?UTF-8?q?=E2=80=94=E8=BD=AE=E8=AF=A2=E5=9C=A8=E4=BB=BB=E5=8A=A1=E7=BB=88?= =?UTF-8?q?=E6=80=81=E5=8D=B3=E5=81=9C=EF=BC=8C=E8=80=8C=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=20Excel=20=E7=94=B1=20Java=20=E4=BE=A7=20MQ=20=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E7=94=9F=E6=88=90=EF=BC=8C=E4=B8=A4=E8=80=85=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E7=AA=97=E5=86=85=E5=89=8D=E7=AB=AF=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E7=9A=84=20fileReady=20=E6=B0=B8=E8=BF=9C=E5=81=9C=E5=9C=A8=20?= =?UTF-8?q?false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:任务 27547(用户 996)15:06:31 创建、15:12:20 完成,结果文件 15:12:20 已生成并上传 OSS (result/patrol_delete/.../巡店删除-27547.xlsx,4033 字节,直链与 /results/{id}/download 均 200), 但页面上始终没有下载入口,用户只能靠刷新页面才刷出来。 排查(生产日志时间线):结果文件任务 15:12:10 入队(MQ)、15:12:20 完成;前端最后一次拉取 history/progress 是 15:12:12,之后客户端再无任何请求——即前端是在 fileReady 尚为 false 时停的轮询。 根因:waitForTaskTerminal 只认 taskStatus(SUCCESS/FAILED)就 return,随后 clearActiveQueueTask() 令 hasQueueWork=false、stopHistoryPolling() 停掉历史轮询,refreshTaskViews() 也只跑这一轮。 taskStatus 是 Python 端回传完毕的信号,而 Excel 是 Java 收尾后(finalizeTaskWorkbook → enqueueAssembleResult)异步生成的,中间有数秒到数十秒间隔;错过就再也不会刷新,本地 fileReady 永远为 false,canDownload() 恒假 → 历史记录里不渲染「下载结果」按钮。 修复: ① 新增 isResultFilePending():判定「确实有文件任务在生成」(fileJobId 存在或 fileStatus=PENDING/RUNNING),文件已就绪(fileReady/downloadUrl)或已终态 (SUCCESS/FAILED)则不再等待;全部店铺失败的任务本就不生成文件(fileStatus 为空),不会被误等。 ② waitForTaskTerminal 在任务终态后继续轮询至文件就绪,上限 MAX_FILE_WAIT_ROUNDS=12(≈2 分钟), 期间 queuePushResult 提示「正在生成结果文件(n/12)」,超时打 warn 后按终态返回,不阻塞队列。 ③ 新增 settlePendingResultFiles():页面加载时若历史里存在「任务已完成、文件仍在生成」的记录 (在生成窗口内刷新页面/重开客户端的场景),继续轮询收敛,避免同一个坑的另一个入口。 仅在无未完成任务时执行,避免与 processQueue 重复轮询。 与同类页面对齐:BrandAppearancePatentTab 早已用 pendingFileTaskIds 等文件就绪再停轮询, 巡店删除页缺这一环。日志按规范用中文,关键分支(等待/超时/收敛完成)均有输出便于线上排查。 --- .../brand/components/BrandPatrolDeleteTab.vue | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue b/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue index e1ae7bf9..ff438458 100644 --- a/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue @@ -274,6 +274,8 @@ import { } from "@/shared/country-options"; const MAX_TRANSIENT_ERRORS = 30; +/** 任务终态后等待结果文件(Java 侧异步生成)的最大轮次,12 × 10s ≈ 2 分钟 */ +const MAX_FILE_WAIT_ROUNDS = 12; const ziniaoVersion = useZiniaoVersion(); const shopInput = ref(""); @@ -624,6 +626,25 @@ function canDownload(item: PatrolDeleteHistoryItem) { ); } +/** + * 结果文件是否仍在生成中。 + * + * 任务终态(taskStatus=SUCCESS)只代表 Python 端回传完毕,结果 Excel 由 Java 侧 + * 收尾后经 MQ 异步生成(finalizeTaskWorkbook → enqueueAssembleResult),两步之间 + * 有几秒到几十秒的间隔。只按 taskStatus 停轮询会让前端本地的 fileReady 永远停在 + * false,历史记录里就不会出现「下载结果」按钮(任务 27547 即此现象)。 + * + * 判定只认「确实有文件任务」的信号(fileJobId / PENDING / RUNNING):全部店铺失败 + * 的任务不会生成结果文件,其 fileStatus 为空,不应继续等待。 + */ +function isResultFilePending(item?: PatrolDeleteHistoryItem) { + if (!item) return false; + if (item.fileReady || item.downloadUrl) return false; + const fileStatus = String(item.fileStatus || "").toUpperCase(); + if (fileStatus === "SUCCESS" || fileStatus === "FAILED") return false; + return item.fileJobId != null || fileStatus === "PENDING" || fileStatus === "RUNNING"; +} + function saveMatchedItems() { if (typeof window === "undefined") return; window.localStorage.setItem( @@ -965,6 +986,7 @@ function findHistoryItemByTaskId(taskId: number) { async function waitForTaskTerminal(taskId: number) { let transientErrorCount = 0; + let fileWaitRounds = 0; while (true) { if (disposed) return "STOPPED"; if (activeTaskId.value !== taskId) return "DELETED"; @@ -982,6 +1004,22 @@ async function waitForTaskTerminal(taskId: number) { const item = findHistoryItemByTaskId(taskId); const status = item?.taskStatus || ""; if (status === "SUCCESS" || status === "FAILED" || status === "COMPLETED") { + // 任务终态后结果 Excel 还在异步生成,必须继续轮询到文件就绪再退出: + // 一旦退出,clearActiveQueueTask 会停掉历史轮询,fileReady 就再也刷不出来了 + if (isResultFilePending(item) && fileWaitRounds < MAX_FILE_WAIT_ROUNDS) { + fileWaitRounds += 1; + console.log( + `[patrol-delete] 任务 ${taskId} 已终态(${status}),等待结果文件生成 ${fileWaitRounds}/${MAX_FILE_WAIT_ROUNDS}`, + ); + queuePushResult.value = `任务 ${taskId} 已完成,正在生成结果文件(${fileWaitRounds}/${MAX_FILE_WAIT_ROUNDS})...`; + await sleep(getTaskPollIntervalMs()); + continue; + } + if (isResultFilePending(item)) { + console.warn( + `[patrol-delete] 任务 ${taskId} 结果文件等待超时(${MAX_FILE_WAIT_ROUNDS} 轮),按任务终态返回,可刷新页面查看`, + ); + } return status; } } catch (error) { @@ -1000,6 +1038,36 @@ async function waitForTaskTerminal(taskId: number) { } } +/** + * 页面加载时收敛:历史里有「任务已完成但结果文件仍在生成」的任务时(例如正好在生成 + * 窗口内刷新页面 / 重开客户端),继续轮询到文件就绪,避免下载按钮不出现。 + */ +async function settlePendingResultFiles() { + for (let round = 1; round <= MAX_FILE_WAIT_ROUNDS; round += 1) { + const pendingIds = Array.from(new Set( + taskRecordItems.value + .filter((item) => isTaskTerminal(item.taskStatus) && isResultFilePending(item)) + .map((item) => item.taskId) + .filter((taskId): taskId is number => typeof taskId === "number" && taskId > 0), + )); + if (!pendingIds.length) { + if (round > 1) { + console.log(`[patrol-delete] 结果文件已就绪(第 ${round - 1} 轮收敛完成)`); + } + return; + } + console.log( + `[patrol-delete] 任务 ${pendingIds.join(",")} 结果文件仍在生成,等待中 ${round}/${MAX_FILE_WAIT_ROUNDS}`, + ); + await sleep(getTaskPollIntervalMs()); + if (disposed) return; + await refreshActiveTaskProgress(pendingIds); + } + console.warn( + `[patrol-delete] 结果文件等待超时(${MAX_FILE_WAIT_ROUNDS} 轮),可稍后刷新页面查看下载按钮`, + ); +} + async function processQueue() { if (disposed || queueWorkerRunning.value) return; queueWorkerRunning.value = true; @@ -1196,6 +1264,9 @@ onMounted(async () => { ? `检测到未完成任务,继续等待任务 ${activeTaskId.value} 完成` : ""; void processQueue(); + } else { + // 未完成任务由 processQueue 负责等待;这里只收敛「任务已完成、结果文件还在生成」的历史任务 + void settlePendingResultFiles(); } });