diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java index 70f17c8c..debe652b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java @@ -54,6 +54,7 @@ public class TaskHeartbeatService { private static final String MODULE_BRAND = "BRAND"; private final FileTaskMapper fileTaskMapper; + private final TaskProgressSnapshotService taskProgressSnapshotService; private final BrandCrawlTaskMapper brandCrawlTaskMapper; private final ProductRiskTaskCacheService productRiskTaskCacheService; private final PublishTaskService publishTaskService; @@ -142,6 +143,18 @@ public class TaskHeartbeatService { if (updated > 0) { log.warn("[task-interrupted] file task marked failed by client restart taskId={} moduleType={} reason={}", taskId, fileTask.getModuleType(), safeReason); + // 同步模块缓存与进度快照:progress/batch 读缓存/快照,不刷新会让前端 + // 一直按 RUNNING 渲染(任务面板永远"处理中"并阻塞该工具后续任务)。 + try { + fileTask.setStatus("FAILED"); + fileTask.setErrorMessage(safeReason); + fileTask.setFinishedAt(LocalDateTime.now()); + saveFileTaskCache(fileTask.getModuleType(), fileTask); + taskProgressSnapshotService.markTerminal(taskId, fileTask.getModuleType(), "FAILED", safeReason); + } catch (Exception cacheEx) { + log.warn("[task-interrupted] refresh cache/snapshot failed taskId={} err={}", + taskId, cacheEx.getMessage()); + } return TaskHeartbeatVo.notAlive(fileTask.getModuleType(), "FAILED", "marked failed"); } log.info("[task-interrupted] file task not in RUNNING, skipped taskId={} status={}", taskId, status); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskProgressSnapshotService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskProgressSnapshotService.java index 92fcf07c..959f3a44 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskProgressSnapshotService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskProgressSnapshotService.java @@ -157,6 +157,31 @@ public class TaskProgressSnapshotService { lastWriteAtMillis.remove(cacheKey(taskId, moduleType)); } + /** + * 任务被外部置为终态(客户端中断上报 / stale 兜底修复)时同步进度快照。 + * 不同步的话,progress/batch 仍按快照里的 RUNNING 回放,前端任务面板永远"处理中" + * 并阻塞该工具的后续任务(2026-09-13 真机:客户端重启后店铺数据采集一直显示 28094 处理中)。 + */ + @Transactional + public void markTerminal(Long taskId, String moduleType, String status, String message) { + if (taskId == null || taskId <= 0 || isBlank(moduleType) || isBlank(status)) { + return; + } + TaskProgressSnapshotEntity existing = find(taskId, moduleType); + if (existing == null) { + return; + } + if (Objects.equals(existing.getStatus(), status) && Objects.equals(existing.getMessage(), message)) { + return; + } + taskProgressSnapshotMapper.update(null, new LambdaUpdateWrapper() + .eq(TaskProgressSnapshotEntity::getId, existing.getId()) + .set(TaskProgressSnapshotEntity::getStatus, status) + .set(TaskProgressSnapshotEntity::getMessage, message) + .set(TaskProgressSnapshotEntity::getUpdatedAt, LocalDateTime.now())); + lastWriteAtMillis.remove(cacheKey(taskId, moduleType)); + } + private String writeJson(Object value) { try { return objectMapper.writeValueAsString(value);