fix(patrol-delete): 任务跑完不出现「下载结果」按钮——轮询在任务终态即停,而结果 Excel 由 Java 侧 MQ 异步生成,两者时间窗内前端本地的 fileReady 永远停在 false
现象:任务 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 等文件就绪再停轮询,
巡店删除页缺这一环。日志按规范用中文,关键分支(等待/超时/收敛完成)均有输出便于线上排查。
This commit is contained in:
@@ -274,6 +274,8 @@ import {
|
|||||||
} from "@/shared/country-options";
|
} from "@/shared/country-options";
|
||||||
|
|
||||||
const MAX_TRANSIENT_ERRORS = 30;
|
const MAX_TRANSIENT_ERRORS = 30;
|
||||||
|
/** 任务终态后等待结果文件(Java 侧异步生成)的最大轮次,12 × 10s ≈ 2 分钟 */
|
||||||
|
const MAX_FILE_WAIT_ROUNDS = 12;
|
||||||
const ziniaoVersion = useZiniaoVersion();
|
const ziniaoVersion = useZiniaoVersion();
|
||||||
|
|
||||||
const shopInput = ref("");
|
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() {
|
function saveMatchedItems() {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@@ -965,6 +986,7 @@ function findHistoryItemByTaskId(taskId: number) {
|
|||||||
|
|
||||||
async function waitForTaskTerminal(taskId: number) {
|
async function waitForTaskTerminal(taskId: number) {
|
||||||
let transientErrorCount = 0;
|
let transientErrorCount = 0;
|
||||||
|
let fileWaitRounds = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (disposed) return "STOPPED";
|
if (disposed) return "STOPPED";
|
||||||
if (activeTaskId.value !== taskId) return "DELETED";
|
if (activeTaskId.value !== taskId) return "DELETED";
|
||||||
@@ -982,6 +1004,22 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
const item = findHistoryItemByTaskId(taskId);
|
const item = findHistoryItemByTaskId(taskId);
|
||||||
const status = item?.taskStatus || "";
|
const status = item?.taskStatus || "";
|
||||||
if (status === "SUCCESS" || status === "FAILED" || status === "COMPLETED") {
|
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;
|
return status;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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() {
|
async function processQueue() {
|
||||||
if (disposed || queueWorkerRunning.value) return;
|
if (disposed || queueWorkerRunning.value) return;
|
||||||
queueWorkerRunning.value = true;
|
queueWorkerRunning.value = true;
|
||||||
@@ -1196,6 +1264,9 @@ onMounted(async () => {
|
|||||||
? `检测到未完成任务,继续等待任务 ${activeTaskId.value} 完成`
|
? `检测到未完成任务,继续等待任务 ${activeTaskId.value} 完成`
|
||||||
: "";
|
: "";
|
||||||
void processQueue();
|
void processQueue();
|
||||||
|
} else {
|
||||||
|
// 未完成任务由 processQueue 负责等待;这里只收敛「任务已完成、结果文件还在生成」的历史任务
|
||||||
|
void settlePendingResultFiles();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user