feat(downloads): expose pending export retry
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<HTMLButtonElement>('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,
|
||||
|
||||
@@ -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(() => {
|
||||
<template #actions="{ row }"><div class="table-actions">
|
||||
<el-button v-if="(row as PlatformExportTask)?.id" link type="primary" @click="openDetail(row as PlatformExportTask)"><Eye :size="15" />详情</el-button>
|
||||
<el-button v-if="(row as PlatformExportTask).status === 'SUCCEEDED'" link type="primary" :loading="downloadingId === (row as PlatformExportTask).id" @click="downloadTask(row as PlatformExportTask)"><Download :size="15" />下载</el-button>
|
||||
<el-button v-if="canRetry(row as PlatformExportTask)" link type="primary" :loading="retryingId === (row as PlatformExportTask).id" @click="retryTask(row as PlatformExportTask)"><RotateCcw :size="15" />重试</el-button>
|
||||
<el-button v-if="canRetry(row as PlatformExportTask)" link type="primary" :loading="retryingId === (row as PlatformExportTask).id" @click="retryTask(row as PlatformExportTask)"><RotateCcw :size="15" />{{ retryLabel(row as PlatformExportTask) }}</el-button>
|
||||
</div></template>
|
||||
<template #type="{ row }">{{ typeLabel((row as PlatformExportTask).exportType) }}</template>
|
||||
<template #status="{ row }"><el-tag :type="statusMeta((row as PlatformExportTask).status).type" effect="light">{{ statusMeta((row as PlatformExportTask).status).label }}</el-tag></template>
|
||||
<template #status="{ row }"><el-tag :type="taskStatusMeta(row as PlatformExportTask).type" effect="light">{{ taskStatusMeta(row as PlatformExportTask).label }}</el-tag></template>
|
||||
<template #progress="{ row }"><span>{{ (row as PlatformExportTask).processedRows }}<template v-if="(row as PlatformExportTask).totalRows !== null"> / {{ (row as PlatformExportTask).totalRows }}</template></span><small v-if="(row as PlatformExportTask).sizeBytes !== null">{{ formatSize((row as PlatformExportTask).sizeBytes) }}</small></template>
|
||||
<template #requester="{ row }">{{ (row as PlatformExportTask).requestedUsername || (row as PlatformExportTask).requestedBy }}</template>
|
||||
<template #requestedAt="{ row }">{{ formatDate((row as PlatformExportTask).requestedAt) }}</template>
|
||||
@@ -309,7 +321,7 @@ onBeforeUnmount(() => {
|
||||
<h3>{{ detailTask.taskName }}</h3>
|
||||
<p>{{ typeLabel(detailTask.exportType) }} · {{ detailTask.format }}</p>
|
||||
</div>
|
||||
<el-tag :type="statusMeta(detailTask.status).type" effect="light">{{ statusMeta(detailTask.status).label }}</el-tag>
|
||||
<el-tag :type="taskStatusMeta(detailTask).type" effect="light">{{ taskStatusMeta(detailTask).label }}</el-tag>
|
||||
</div>
|
||||
<dl class="task-detail-grid">
|
||||
<div><dt>任务编号</dt><dd>{{ detailTask.taskNo }}</dd></div>
|
||||
@@ -317,6 +329,8 @@ onBeforeUnmount(() => {
|
||||
<div><dt>申请时间</dt><dd>{{ formatDate(detailTask.requestedAt) }}</dd></div>
|
||||
<div><dt>开始时间</dt><dd>{{ formatDate(detailTask.startedAt) }}</dd></div>
|
||||
<div><dt>完成时间</dt><dd>{{ formatDate(detailTask.completedAt) }}</dd></div>
|
||||
<div><dt>执行次数</dt><dd>{{ detailTask.attemptCount }} / {{ detailTask.maxAttempts }}</dd></div>
|
||||
<div><dt>下次重试</dt><dd>{{ detailTask.status === 'PENDING' && detailTask.attemptCount > 0 ? formatDate(detailTask.nextRetryAt) : '—' }}</dd></div>
|
||||
<div><dt>过期时间</dt><dd>{{ formatDate(detailTask.expiresAt) }}</dd></div>
|
||||
<div><dt>处理数据</dt><dd>{{ detailTask.processedRows }}<template v-if="detailTask.totalRows !== null"> / {{ detailTask.totalRows }}</template> 行</dd></div>
|
||||
<div><dt>文件大小</dt><dd>{{ formatSize(detailTask.sizeBytes) || '—' }}</dd></div>
|
||||
@@ -330,6 +344,11 @@ onBeforeUnmount(() => {
|
||||
<template #footer="{ close }">
|
||||
<div class="app-dialog__footer">
|
||||
<el-button @click="close">关闭</el-button>
|
||||
<el-button
|
||||
v-if="detailTask && canRetry(detailTask)"
|
||||
:loading="retryingId === detailTask.id"
|
||||
@click="retryTask(detailTask)"
|
||||
>{{ retryLabel(detailTask) }}</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="downloadingId === detailTask?.id"
|
||||
|
||||
Reference in New Issue
Block a user