diff --git a/src/modules/downloads/api/platformExportTasks.ts b/src/modules/downloads/api/platformExportTasks.ts index f340038f..c76fa186 100644 --- a/src/modules/downloads/api/platformExportTasks.ts +++ b/src/modules/downloads/api/platformExportTasks.ts @@ -28,6 +28,9 @@ export interface PlatformExportTask { contentType: string | null sizeBytes: number | null failureMessage: string | null + attemptCount: number + maxAttempts: number + nextRetryAt: string | null requestedBy: string requestedUsername: string | null requestedAt: string diff --git a/src/modules/downloads/pages/DownloadCenterView.test.ts b/src/modules/downloads/pages/DownloadCenterView.test.ts index 209075d2..fc5ec843 100644 --- a/src/modules/downloads/pages/DownloadCenterView.test.ts +++ b/src/modules/downloads/pages/DownloadCenterView.test.ts @@ -30,7 +30,8 @@ const task: PlatformExportTask = { id: '9', taskNo: 'EXP9', exportType: 'CUSTOMER_LIST', format: 'XLSX', taskName: '客户列表', status: 'SUCCEEDED', totalRows: 12, processedRows: 12, fileName: '客户列表.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', sizeBytes: 2048, - failureMessage: null, requestedBy: '1', requestedUsername: 'admin', requestedAt: '2026-08-03T10:00:00', + failureMessage: null, attemptCount: 1, maxAttempts: 3, nextRetryAt: null, + requestedBy: '1', requestedUsername: 'admin', requestedAt: '2026-08-03T10:00:00', startedAt: '2026-08-03T10:00:01', completedAt: '2026-08-03T10:00:02', expiresAt: '2026-08-10T10:00:02', } @@ -85,7 +86,7 @@ describe('DownloadCenterView', () => { expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:export') }) - it('shows retry only for failed tasks with the source export permission', async () => { + it('shows retry for failed tasks with the source export permission', async () => { vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [{ ...task, status: 'FAILED', failureMessage: 'IAM timeout' }], page: 1, size: 20, total: 1, pages: 1 }) const pinia = setActivePinia(createPinia()) const auth = useAuthStore() @@ -102,6 +103,27 @@ describe('DownloadCenterView', () => { expect(retryPlatformExportTask).toHaveBeenCalledWith('9') }) + it('shows waiting retry and allows an immediate retry during automatic backoff', async () => { + vi.mocked(listPlatformExportTasks).mockResolvedValue({ + records: [{ ...task, status: 'PENDING', processedRows: 0, failureMessage: '头程服务查询失败', attemptCount: 2, nextRetryAt: '2026-09-03T10:34:28' }], + page: 1, size: 20, total: 1, pages: 1, + }) + const pinia = setActivePinia(createPinia()) + const auth = useAuthStore() + auth.token = 'token' + auth.permissions = ['platform:download:list', 'platform:customer:export'] + useAccessPreviewStore().initializeAfterLogin() + mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) + await flushPromises() + + expect(document.body.textContent).toContain('等待重试') + const retry = [...document.querySelectorAll('button')] + .find((item) => item.textContent?.trim() === '立即重试') + retry?.click() + await flushPromises() + expect(retryPlatformExportTask).toHaveBeenCalledWith('9') + }) + it('labels and retries Finance tasks with their exact source export permission', async () => { const financeTask = { ...task, diff --git a/src/modules/downloads/pages/DownloadCenterView.vue b/src/modules/downloads/pages/DownloadCenterView.vue index c5b3badf..2ba8b3b1 100644 --- a/src/modules/downloads/pages/DownloadCenterView.vue +++ b/src/modules/downloads/pages/DownloadCenterView.vue @@ -222,6 +222,13 @@ function statusMeta(status: ExportTaskStatus) { return values[status] ?? { label: status || '未知状态', type: 'info' as const } } +function taskStatusMeta(task: PlatformExportTask) { + if (task.status === 'PENDING' && task.attemptCount > 0) { + return { label: '等待重试', type: 'warning' as const } + } + return statusMeta(task.status) +} + function formatDate(value: string | null) { if (!value) return '—' const date = new Date(value) @@ -237,7 +244,12 @@ function formatSize(bytes: number | null) { function canRetry(task: PlatformExportTask) { const permission = exportTypeMeta[task.exportType]?.retryPermission - return task.status === 'FAILED' && permission !== undefined && access.can(permission) + const retryableStatus = task.status === 'FAILED' || (task.status === 'PENDING' && task.attemptCount > 0) + return retryableStatus && permission !== undefined && access.can(permission) +} + +function retryLabel(task: PlatformExportTask) { + return task.status === 'PENDING' ? '立即重试' : '重试' } watch([page, pageSize], () => { void loadTasks() }) @@ -275,10 +287,10 @@ onBeforeUnmount(() => { - + @@ -309,7 +321,7 @@ onBeforeUnmount(() => {

{{ detailTask.taskName }}

{{ typeLabel(detailTask.exportType) }} · {{ detailTask.format }}

- {{ statusMeta(detailTask.status).label }} + {{ taskStatusMeta(detailTask).label }}
任务编号
{{ detailTask.taskNo }}
@@ -317,6 +329,8 @@ onBeforeUnmount(() => {
申请时间
{{ formatDate(detailTask.requestedAt) }}
开始时间
{{ formatDate(detailTask.startedAt) }}
完成时间
{{ formatDate(detailTask.completedAt) }}
+
执行次数
{{ detailTask.attemptCount }} / {{ detailTask.maxAttempts }}
+
下次重试
{{ detailTask.status === 'PENDING' && detailTask.attemptCount > 0 ? formatDate(detailTask.nextRetryAt) : '—' }}
过期时间
{{ formatDate(detailTask.expiresAt) }}
处理数据
{{ detailTask.processedRows }} 行
文件大小
{{ formatSize(detailTask.sizeBytes) || '—' }}
@@ -330,6 +344,11 @@ onBeforeUnmount(() => {