fix: 修正自动导入完成提示
- 根据关联文档真实状态识别已完成的历史失败批次 - 完成后自动移除提示并低频复查失败或中断批次
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据批次项及其关联文档的真实状态判断失败批次是否已经完成。
|
||||
*
|
||||
* <p>正常完成批次直接返回成功;仅对部分失败或中断批次执行补充查询,
|
||||
* 避免运行中轮询产生额外数据库压力。批次项数量不完整、文档缺失、
|
||||
* 跨知识库或文档仍未完成时均保持失败提示。</p>
|
||||
*
|
||||
* @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<DocumentImportBatchItem> 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<BigInteger> unresolvedDocumentIds = new LinkedHashSet<BigInteger>();
|
||||
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<tech.easyflow.ai.entity.Document> 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<BigInteger> completedDocumentIds = completedDocuments.stream()
|
||||
.map(tech.easyflow.ai.entity.Document::getId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
return completedDocumentIds.containsAll(unresolvedDocumentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证上传领取通过单条多表更新同步刷新批次进度时间。
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<typeof setTimeout> = 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();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user