fix: 完善自动导入异常中断与恢复
- 自动导入基础设施异常触发批次熔断,保留完整日志并输出安全错误信息 - 增加恢复令牌与租约围栏、无文档失败项重建及消息退避机制 - 前端展示中断状态并在状态请求失败后自动恢复轮询 - 补充批次中断迁移、配置与并发异常路径测试
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user