fix: 完善自动导入异常中断与恢复

- 自动导入基础设施异常触发批次熔断,保留完整日志并输出安全错误信息

- 增加恢复令牌与租约围栏、无文档失败项重建及消息退避机制

- 前端展示中断状态并在状态请求失败后自动恢复轮询

- 补充批次中断迁移、配置与并发异常路径测试
This commit is contained in:
2026-08-10 11:26:00 +08:00
parent 54d85ae460
commit bae7b18977
33 changed files with 3725 additions and 221 deletions

View File

@@ -59,4 +59,35 @@ describe('page data recovery', () => {
expect(get).toHaveBeenCalledTimes(2);
expect(wrapper.text()).not.toContain('数据加载失败,请重试');
});
it('coalesces repeated reloads while a page request is still running', async () => {
let resolveInitialRequest: (value: {
data: { records: never[]; totalRow: number };
}) => void = () => {};
const initialRequest = new Promise<{
data: { records: never[]; totalRow: number };
}>((resolve) => {
resolveInitialRequest = resolve;
});
const get = vi
.fn()
.mockReturnValueOnce(initialRequest)
.mockResolvedValue({ data: { records: [], totalRow: 0 } });
const wrapper = mount(PageData, {
global: {
directives: { loading: {} },
},
props: { pageUrl: '/page', requestClient: { get } },
});
const firstReload = (wrapper.vm as any).reload({ silent: true });
const secondReload = (wrapper.vm as any).reload({ silent: true });
expect(get).toHaveBeenCalledTimes(1);
resolveInitialRequest({ data: { records: [], totalRow: 0 } });
await Promise.all([firstReload, secondReload]);
await flushPromises();
expect(get).toHaveBeenCalledTimes(2);
});
});

View File

@@ -24,6 +24,15 @@ interface PageDataState {
pageSize: number;
}
interface PageDataReloadOptions {
silent?: boolean;
}
interface PageDataRequest {
silent: boolean;
version: number;
}
const props = withDefaults(defineProps<PageDataProps>(), {
pageSize: 10,
pageSizes: () => [10, 20, 50, 100],
@@ -41,7 +50,9 @@ const pageList = ref<PageDataRow[]>([]);
const loading = ref(false);
const loadError = ref<unknown>();
const queryParams = ref<Record<string, any>>({ ...props.initialQueryParams });
let pageRequest = 0;
let activePageRequest: null | Promise<void> = null;
let pendingPageRequest: null | PageDataRequest = null;
let pageRequestVersion = 0;
const pageInfo = reactive({
pageNumber: Math.max(1, Math.trunc(props.initialPageNumber)),
@@ -60,11 +71,7 @@ const doGet = async (params: Record<string, any>) => {
return { data };
};
// 获取页面数据
const getPageList = async () => {
const request = ++pageRequest;
loading.value = true;
loadError.value = undefined;
const loadPageListOnce = async (request: PageDataRequest) => {
try {
const res = await doGet({
pageNumber: pageInfo.pageNumber,
@@ -72,21 +79,61 @@ const getPageList = async () => {
...props.extraQueryParams,
...queryParams.value,
});
if (request === pageRequest) {
if (request.version === pageRequestVersion) {
pageList.value = res.data?.records || [];
pageInfo.total = res.data?.totalRow || 0;
}
} catch (error) {
if (request === pageRequest) {
if (request.version === pageRequestVersion) {
loadError.value = error;
pageList.value = [];
pageInfo.total = 0;
if (!request.silent || pageList.value.length === 0) {
pageList.value = [];
pageInfo.total = 0;
}
}
} finally {
if (request === pageRequest) loading.value = false;
}
};
const requestPageList = (silent: boolean) => {
const request: PageDataRequest = {
silent: silent && pageList.value.length > 0,
version: ++pageRequestVersion,
};
loadError.value = undefined;
if (!request.silent) {
loading.value = true;
}
if (activePageRequest) {
pendingPageRequest = pendingPageRequest
? {
silent: pendingPageRequest.silent && request.silent,
version: request.version,
}
: request;
return activePageRequest;
}
const drainPageRequests = async () => {
let currentRequest: null | PageDataRequest = request;
while (currentRequest) {
pendingPageRequest = null;
await loadPageListOnce(currentRequest);
currentRequest = pendingPageRequest;
}
};
activePageRequest = drainPageRequests().finally(() => {
activePageRequest = null;
loading.value = false;
});
return activePageRequest;
};
// 获取页面数据
const getPageList = () => requestPageList(false);
const reload = (options: PageDataReloadOptions = {}) =>
requestPageList(Boolean(options.silent));
// 分页事件处理
const handleSizeChange = (newSize: number) => {
pageInfo.pageSize = newSize;
@@ -138,7 +185,7 @@ const setQuery = (newQueryParams: Record<string, any>) => {
// 暴露方法给父组件
defineExpose({
getPageState,
reload: getPageList,
reload,
patchRowById,
setQuery,
});