From 1870ac40280fdfe38c9901c2c8ec5c102e845d24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com>
Date: Wed, 26 Aug 2026 18:58:42 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E8=87=AA=E5=8A=A8?=
=?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=AE=8C=E6=88=90=E6=8F=90=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 根据关联文档真实状态识别已完成的历史失败批次
- 完成后自动移除提示并低频复查失败或中断批次
---
.../DocumentImportBatchDtos.java | 19 +++++
.../task/DocumentImportBatchAppService.java | 75 ++++++++++++++++-
.../DocumentImportBatchAppServiceTest.java | 84 +++++++++++++++++++
.../DocumentImportBatchStatus.test.ts | 64 ++++++++++++--
.../DocumentImportBatchStatus.vue | 24 +++---
5 files changed, 248 insertions(+), 18 deletions(-)
diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/DocumentImportBatchDtos.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/DocumentImportBatchDtos.java
index a0149fb6..e7bd7c8c 100644
--- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/DocumentImportBatchDtos.java
+++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/DocumentImportBatchDtos.java
@@ -252,6 +252,7 @@ public final class DocumentImportBatchDtos {
private BigInteger batchId;
private String importMode;
private String status;
+ private Boolean actualCompleted;
private Integer totalCount;
private Long totalBytes;
private Integer completedCount;
@@ -292,6 +293,24 @@ public final class DocumentImportBatchDtos {
this.status = status;
}
+ /**
+ * 获取批次关联任务是否已经按真实文档状态全部完成。
+ *
+ * @return 全部完成时返回 {@code true}
+ */
+ public Boolean getActualCompleted() {
+ return actualCompleted;
+ }
+
+ /**
+ * 设置批次关联任务是否已经按真实文档状态全部完成。
+ *
+ * @param actualCompleted 是否全部完成
+ */
+ public void setActualCompleted(Boolean actualCompleted) {
+ this.actualCompleted = actualCompleted;
+ }
+
public Integer getTotalCount() {
return totalCount;
}
diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppService.java
index c7d07703..e3ad7e5a 100644
--- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppService.java
+++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppService.java
@@ -21,6 +21,7 @@ import tech.easyflow.ai.enums.DocumentImportBatchItemStage;
import tech.easyflow.ai.enums.DocumentImportBatchItemStatus;
import tech.easyflow.ai.enums.DocumentImportBatchStatus;
import tech.easyflow.ai.enums.DocumentImportMode;
+import tech.easyflow.ai.enums.DocumentProcessStatus;
import tech.easyflow.ai.mapper.DocumentImportBatchItemMapper;
import tech.easyflow.ai.mapper.DocumentImportBatchMapper;
import tech.easyflow.ai.mapper.DocumentMapper;
@@ -435,7 +436,79 @@ public class DocumentImportBatchAppService {
.orderBy(DocumentImportBatch::getCreated, false)
.limit(1)
);
- return batch == null ? null : batchTracker.toStatusResponse(batch);
+ if (batch == null) {
+ return null;
+ }
+ DocumentImportBatchDtos.StatusResponse response =
+ batchTracker.toStatusResponse(batch);
+ response.setActualCompleted(isAutoBatchActuallyCompleted(batch));
+ return response;
+ }
+
+ /**
+ * 根据批次项及其关联文档的真实状态判断失败批次是否已经完成。
+ *
+ *
正常完成批次直接返回成功;仅对部分失败或中断批次执行补充查询,
+ * 避免运行中轮询产生额外数据库压力。批次项数量不完整、文档缺失、
+ * 跨知识库或文档仍未完成时均保持失败提示。
+ *
+ * @param batch 自动导入批次
+ * @return 批次关联任务是否已经全部完成
+ */
+ private boolean isAutoBatchActuallyCompleted(DocumentImportBatch batch) {
+ if (DocumentImportBatchStatus.COMPLETED.name().equals(batch.getStatus())) {
+ return true;
+ }
+ if (!DocumentImportBatchStatus.PARTIAL_SUCCEEDED.name().equals(batch.getStatus())
+ && !DocumentImportBatchStatus.INTERRUPTED.name().equals(batch.getStatus())) {
+ return false;
+ }
+ List items = itemService.list(
+ QueryWrapper.create()
+ .eq(DocumentImportBatchItem::getBatchId, batch.getId())
+ .orderBy(DocumentImportBatchItem::getId, true)
+ );
+ int totalCount = valueOrZero(batch.getTotalCount());
+ if (items == null || totalCount <= 0 || items.size() != totalCount) {
+ return false;
+ }
+
+ Set unresolvedDocumentIds = new LinkedHashSet();
+ for (DocumentImportBatchItem item : items) {
+ String itemStatus = item.getStatus();
+ if (DocumentImportBatchItemStatus.COMPLETED.name().equals(itemStatus)
+ || DocumentImportBatchItemStatus.SKIPPED.name().equals(itemStatus)
+ || DocumentImportBatchItemStatus.CANCELLED.name().equals(itemStatus)) {
+ continue;
+ }
+ if (item.getDocumentId() == null
+ || !batch.getKnowledgeId().equals(item.getKnowledgeId())) {
+ return false;
+ }
+ unresolvedDocumentIds.add(item.getDocumentId());
+ }
+ if (unresolvedDocumentIds.isEmpty()) {
+ return true;
+ }
+
+ List completedDocuments =
+ documentMapper.selectListByQuery(
+ QueryWrapper.create()
+ .select(tech.easyflow.ai.entity.Document::getId)
+ .eq(tech.easyflow.ai.entity.Document::getCollectionId,
+ batch.getKnowledgeId())
+ .eq(tech.easyflow.ai.entity.Document::getProcessStatus,
+ DocumentProcessStatus.COMPLETED.name())
+ .in(tech.easyflow.ai.entity.Document::getId,
+ unresolvedDocumentIds)
+ );
+ if (completedDocuments == null) {
+ return false;
+ }
+ Set completedDocumentIds = completedDocuments.stream()
+ .map(tech.easyflow.ai.entity.Document::getId)
+ .collect(java.util.stream.Collectors.toSet());
+ return completedDocumentIds.containsAll(unresolvedDocumentIds);
}
/**
diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppServiceTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppServiceTest.java
index d1715b5b..6fd45211 100644
--- a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppServiceTest.java
+++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/documentimport/task/DocumentImportBatchAppServiceTest.java
@@ -19,6 +19,7 @@ import tech.easyflow.ai.enums.DocumentImportBatchItemStage;
import tech.easyflow.ai.enums.DocumentImportBatchItemStatus;
import tech.easyflow.ai.enums.DocumentImportBatchStatus;
import tech.easyflow.ai.enums.DocumentImportMode;
+import tech.easyflow.ai.enums.DocumentProcessStatus;
import tech.easyflow.ai.mapper.DocumentImportBatchItemMapper;
import tech.easyflow.ai.mapper.DocumentImportBatchMapper;
import tech.easyflow.ai.mapper.DocumentMapper;
@@ -47,6 +48,89 @@ import java.util.function.BooleanSupplier;
*/
public class DocumentImportBatchAppServiceTest {
+ /**
+ * 验证历史失败批次关联文档均已完成时返回真实完成标记。
+ */
+ @Test
+ public void latestAutoBatchShouldDetectActuallyCompletedDocuments() {
+ TestContext context = createContext();
+ DocumentImportBatch batch = context.batchService.getOne(
+ QueryWrapper.create()
+ );
+ batch.setImportMode(DocumentImportMode.AUTO.name());
+ batch.setStatus(DocumentImportBatchStatus.PARTIAL_SUCCEEDED.name());
+ batch.setTotalCount(2);
+
+ DocumentImportBatchItem completed = uploadedItem(
+ BigInteger.valueOf(11), batch.getId()
+ );
+ completed.setStatus(DocumentImportBatchItemStatus.COMPLETED.name());
+ completed.setDocumentId(BigInteger.valueOf(101));
+ DocumentImportBatchItem staleFailed = uploadedItem(
+ BigInteger.valueOf(12), batch.getId()
+ );
+ staleFailed.setStatus(DocumentImportBatchItemStatus.FAILED.name());
+ staleFailed.setDocumentId(BigInteger.valueOf(102));
+ Mockito.when(context.itemService.list(Mockito.any(QueryWrapper.class)))
+ .thenReturn(List.of(completed, staleFailed));
+
+ tech.easyflow.ai.entity.Document recovered =
+ new tech.easyflow.ai.entity.Document();
+ recovered.setId(staleFailed.getDocumentId());
+ recovered.setCollectionId(batch.getKnowledgeId());
+ recovered.setProcessStatus(DocumentProcessStatus.COMPLETED.name());
+ Mockito.when(context.documentMapper.selectListByQuery(Mockito.any()))
+ .thenReturn(List.of(recovered));
+
+ DocumentImportBatchDtos.StatusResponse batchResponse =
+ new DocumentImportBatchDtos.StatusResponse();
+ batchResponse.setStatus(batch.getStatus());
+ Mockito.when(context.batchTracker.toStatusResponse(batch))
+ .thenReturn(batchResponse);
+
+ DocumentImportBatchDtos.StatusResponse response =
+ context.service.getLatestAutoBatch(batch.getKnowledgeId());
+
+ Assert.assertTrue(response.getActualCompleted());
+ Mockito.verify(context.documentMapper)
+ .selectListByQuery(Mockito.any(QueryWrapper.class));
+ }
+
+ /**
+ * 验证失败项关联文档仍未完成时继续保留批次提示。
+ */
+ @Test
+ public void latestAutoBatchShouldKeepIncompleteFailureVisible() {
+ TestContext context = createContext();
+ DocumentImportBatch batch = context.batchService.getOne(
+ QueryWrapper.create()
+ );
+ batch.setImportMode(DocumentImportMode.AUTO.name());
+ batch.setStatus(DocumentImportBatchStatus.PARTIAL_SUCCEEDED.name());
+ batch.setTotalCount(1);
+
+ DocumentImportBatchItem failed = uploadedItem(
+ BigInteger.valueOf(13), batch.getId()
+ );
+ failed.setStatus(DocumentImportBatchItemStatus.FAILED.name());
+ failed.setDocumentId(BigInteger.valueOf(103));
+ Mockito.when(context.itemService.list(Mockito.any(QueryWrapper.class)))
+ .thenReturn(List.of(failed));
+ Mockito.when(context.documentMapper.selectListByQuery(Mockito.any()))
+ .thenReturn(List.of());
+
+ DocumentImportBatchDtos.StatusResponse batchResponse =
+ new DocumentImportBatchDtos.StatusResponse();
+ batchResponse.setStatus(batch.getStatus());
+ Mockito.when(context.batchTracker.toStatusResponse(batch))
+ .thenReturn(batchResponse);
+
+ DocumentImportBatchDtos.StatusResponse response =
+ context.service.getLatestAutoBatch(batch.getKnowledgeId());
+
+ Assert.assertFalse(response.getActualCompleted());
+ }
+
/**
* 验证上传领取通过单条多表更新同步刷新批次进度时间。
*/
diff --git a/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.test.ts b/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.test.ts
index 052e4b6b..a3c28fdd 100644
--- a/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.test.ts
+++ b/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.test.ts
@@ -51,7 +51,7 @@ describe('documentImportBatchStatus', () => {
wrapper.unmount();
});
- it('当前页面跟踪的批次完成后继续展示结果', async () => {
+ it('当前页面跟踪的批次完成后移除提示', async () => {
vi.useFakeTimers();
apiMocks.get
.mockResolvedValueOnce({
@@ -73,10 +73,62 @@ describe('documentImportBatchStatus', () => {
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
+ expect(wrapper.find('.batch-status').exists()).toBe(false);
+ wrapper.unmount();
+ });
+
+ it('失败批次关联任务实际完成后不再展示提示', async () => {
+ apiMocks.get.mockResolvedValue({
+ data: {
+ ...createBatch('RUNNING', 0),
+ actualCompleted: true,
+ failedCount: 2,
+ pendingCount: 0,
+ status: 'PARTIAL_SUCCEEDED',
+ },
+ errorCode: 0,
+ });
+
+ const wrapper = mount(DocumentImportBatchStatus, {
+ props: { knowledgeId: 'knowledge-1' },
+ });
+ await flushPromises();
+
+ expect(wrapper.find('.batch-status').exists()).toBe(false);
+ wrapper.unmount();
+ });
+
+ it('失败批次低频复查后在关联任务完成时移除提示', async () => {
+ vi.useFakeTimers();
+ const failedBatch = {
+ ...createBatch('RUNNING', 0),
+ actualCompleted: false,
+ failedCount: 2,
+ pendingCount: 0,
+ status: 'PARTIAL_SUCCEEDED',
+ };
+ apiMocks.get
+ .mockResolvedValueOnce({
+ data: failedBatch,
+ errorCode: 0,
+ })
+ .mockResolvedValueOnce({
+ data: { ...failedBatch, actualCompleted: true },
+ errorCode: 0,
+ });
+
+ const wrapper = mount(DocumentImportBatchStatus, {
+ props: { knowledgeId: 'knowledge-1' },
+ });
+ await flushPromises();
+
expect(wrapper.find('.batch-status').exists()).toBe(true);
- expect(wrapper.text()).toContain(
- 'documentCollection.importDoc.batchCompleted',
- );
+
+ await vi.advanceTimersByTimeAsync(15_000);
+ await flushPromises();
+
+ expect(apiMocks.get).toHaveBeenCalledTimes(2);
+ expect(wrapper.find('.batch-status').exists()).toBe(false);
wrapper.unmount();
});
@@ -239,9 +291,7 @@ describe('documentImportBatchStatus', () => {
expect(apiMocks.get).toHaveBeenCalledTimes(3);
expect(wrapper.find('.batch-status__load-error').exists()).toBe(false);
- expect(wrapper.text()).toContain(
- 'documentCollection.importDoc.batchCompleted',
- );
+ expect(wrapper.find('.batch-status').exists()).toBe(false);
wrapper.unmount();
});
});
diff --git a/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.vue b/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.vue
index d387329e..647e2b46 100644
--- a/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.vue
+++ b/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentImportBatchStatus.vue
@@ -8,6 +8,7 @@ import { ElAlert, ElButton, ElProgress } from 'element-plus';
import { api } from '#/api/request';
interface BatchStatus {
+ actualCompleted?: boolean;
batchId: string;
cancelledCount?: number;
completedCount: number;
@@ -53,6 +54,7 @@ let pollTimer: null | ReturnType = null;
let disposed = false;
let refreshGeneration = 0;
let pollDelayMs = 3000;
+const TERMINAL_RECHECK_DELAY_MS = 15_000;
const canContinue = computed(
() =>
@@ -122,7 +124,7 @@ const interruptMetadata = computed(() => {
return details.join(' · ');
});
-async function refresh(hideCompletedOnRestore = false) {
+async function refresh() {
if (!props.knowledgeId) return;
const currentGeneration = ++refreshGeneration;
refreshing.value = true;
@@ -142,10 +144,9 @@ async function refresh(hideCompletedOnRestore = false) {
loadError.value = '';
pollDelayMs = 3000;
const restoredBatch = response.data || undefined;
- batch.value =
- hideCompletedOnRestore && restoredBatch?.status === 'COMPLETED'
- ? undefined
- : restoredBatch;
+ const completed =
+ restoredBatch?.status === 'COMPLETED' || restoredBatch?.actualCompleted;
+ batch.value = completed ? undefined : restoredBatch;
} catch {
if (!disposed && currentGeneration === refreshGeneration) {
loadError.value = $t(
@@ -175,13 +176,16 @@ function schedulePoll() {
if (
!batch.value ||
batch.value.status === 'COMPLETED' ||
- batch.value.status === 'PARTIAL_SUCCEEDED' ||
- batch.value.status === 'INTERRUPTED' ||
batch.value.status === 'CANCELLED'
) {
return;
}
- pollTimer = setTimeout(refresh, 3000);
+ const delay =
+ batch.value.status === 'PARTIAL_SUCCEEDED' ||
+ batch.value.status === 'INTERRUPTED'
+ ? TERMINAL_RECHECK_DELAY_MS
+ : 3000;
+ pollTimer = setTimeout(refresh, delay);
}
async function continueBatch() {
@@ -204,7 +208,7 @@ async function continueBatch() {
onMounted(() => {
disposed = false;
- refresh(true);
+ refresh();
});
onBeforeUnmount(() => {
@@ -222,7 +226,7 @@ watch(
loadError.value = '';
pollDelayMs = 3000;
}
- refresh(knowledgeChanged);
+ refresh();
},
);