导出任务:新增「取消」(后台检测取消标记后停止并删半成品文件)
This commit is contained in:
parent
4d5b9bf440
commit
ad65f0cc75
@ -40,6 +40,12 @@ class RunAccountBillsExportTask extends Command
|
||||
$task = $tasks->patch($task, ['status' => 'running', 'started_at' => AccountBillsExportTasks::beijingNow()]);
|
||||
|
||||
try {
|
||||
if (! empty($task['cancel'])) {
|
||||
$tasks->patch($task, ['status' => 'cancelled', 'finished_at' => AccountBillsExportTasks::beijingNow()]);
|
||||
$this->info('任务已取消。');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
$columns = array_values((array) ($task['columns'] ?? []));
|
||||
if ($columns === []) {
|
||||
throw new \RuntimeException('导出任务没有可用字段。');
|
||||
@ -62,6 +68,16 @@ class RunAccountBillsExportTask extends Command
|
||||
$written++;
|
||||
if ($written % 2000 === 0) {
|
||||
$task = $tasks->patch($task, ['status' => 'running', 'rows' => $written]);
|
||||
// 用户在弹窗里点了「取消」:停止导出并删掉半成品文件。
|
||||
$fresh = $tasks->find((int) $task['user_id'], (string) $task['token']);
|
||||
if (! empty($fresh['cancel'])) {
|
||||
fclose($csv);
|
||||
@unlink($tasks->csvPath($task));
|
||||
$tasks->patch($task, ['status' => 'cancelled', 'rows' => $written, 'finished_at' => AccountBillsExportTasks::beijingNow(), 'size' => 0]);
|
||||
$this->info('任务已取消(已写入 ' . $written . ' 行后停止)。');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose($csv);
|
||||
|
||||
@ -439,6 +439,23 @@ class AccountBills extends Controller
|
||||
return response()->download($path, $name, ['Content-Type' => 'text/csv; charset=UTF-8']);
|
||||
}
|
||||
|
||||
/** 取消导出任务:写入取消标记,后台进程检测到后停止并删除半成品文件。 */
|
||||
public function cancelExportTask(Request $request, string $token)
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
$tasks = app(\Modules\EcommerceFinance\Services\AccountBillsExportTasks::class);
|
||||
$task = $tasks->find((int) user()->id, $token);
|
||||
abort_if($task === null, 404, '导出任务不存在或已过期。');
|
||||
|
||||
$status = (string) ($task['status'] ?? '');
|
||||
abort_if(in_array($status, ['done', 'failed', 'cancelled'], true), 409, '该任务已结束,无需取消。');
|
||||
|
||||
$tasks->patch($task, ['cancel' => true]);
|
||||
|
||||
return response()->json(['status' => 'ok'], 200, ['Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 该页签可导出的字段:field => label。
|
||||
*
|
||||
|
||||
@ -66,6 +66,7 @@ class AccountBillsExportTasks
|
||||
'started_at' => null,
|
||||
'finished_at' => null,
|
||||
'size' => 0,
|
||||
'cancel' => false,
|
||||
];
|
||||
|
||||
$this->write($task);
|
||||
@ -156,6 +157,7 @@ class AccountBillsExportTasks
|
||||
'total' => (int) ($task['total'] ?? 0),
|
||||
'size' => (int) ($task['size'] ?? 0),
|
||||
'error' => $task['error'] ?? null,
|
||||
'cancel' => (bool) ($task['cancel'] ?? false),
|
||||
'planned_at' => $task['planned_at'] ?? null,
|
||||
'started_at' => $task['started_at'] ?? null,
|
||||
'finished_at' => $task['finished_at'] ?? null,
|
||||
|
||||
@ -82,7 +82,9 @@
|
||||
<td>
|
||||
<a v-if="task.status === 'done'" :href="downloadUrl(task)" class="tw-modal__link">下载</a>
|
||||
<span v-else-if="task.status === 'failed'">{{ task.error || '失败' }}</span>
|
||||
<span v-else>—</span>
|
||||
<template v-else>
|
||||
<button type="button" class="tw-modal__cancel" @click="cancelExportTask(task)">取消</button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!exportTasks.length"><td colspan="8" class="tw-modal__empty">暂无导出任务</td></tr>
|
||||
@ -215,7 +217,15 @@ export default {
|
||||
this.exportTimer=window.setTimeout(()=>this.refreshExportTasks(),3000);
|
||||
},
|
||||
statusLabel(task){
|
||||
return {queued:'排队中',running:'导出中',done:'成功',failed:'失败'}[task.status]||task.status;
|
||||
return {queued:'排队中',running:'导出中',done:'成功',failed:'失败',cancelled:'已取消'}[task.status]||task.status;
|
||||
},
|
||||
async cancelExportTask(task){
|
||||
try{
|
||||
await window.axios.post(`${this.tasksBaseUrl}/${encodeURIComponent(task.token)}/cancel`);
|
||||
await this.refreshExportTasks();
|
||||
}catch(error){
|
||||
this.exportHint=(error.response&&error.response.data&&error.response.data.message)||'取消失败,请重试。';
|
||||
}
|
||||
},
|
||||
downloadUrl(task){
|
||||
return `${this.tasksBaseUrl}/${encodeURIComponent(task.token)}/download`;
|
||||
@ -241,6 +251,8 @@ export default {
|
||||
.tw-modal__table td { padding: .4rem .5rem; border-bottom: 1px solid #eeeae2; white-space: nowrap; }
|
||||
.tw-modal__empty { text-align: center; color: #8a8377; padding: 1.5rem !important; }
|
||||
.tw-modal__link { color: #856632; font-weight: 600; text-decoration: underline; }
|
||||
.tw-modal__cancel { border: 1px solid #d8d2c7; border-radius: .375rem; background: #fff; color: #b80000; font: 500 .75rem/1 inherit; padding: .25rem .5rem; cursor: pointer; }
|
||||
.tw-modal__cancel:hover { border-color: #b80000; }
|
||||
.tw-modal__foot { display: flex; justify-content: flex-end; gap: .5rem; padding: 1rem 1.25rem; border-top: 1px solid #eee7da; }
|
||||
.tw-export-status { padding: .0625rem .375rem; border-radius: .25rem; background: #f2f2f2; color: #595959; }
|
||||
.tw-export-status.is-done { background: #e6f0e0; color: #3f6b2b; }
|
||||
|
||||
@ -90,7 +90,9 @@
|
||||
<td>
|
||||
<a v-if="task.status === 'done'" :href="downloadUrl(task)" class="tw-modal__link">下载</a>
|
||||
<span v-else-if="task.status === 'failed'">{{ task.error || '失败' }}</span>
|
||||
<span v-else>—</span>
|
||||
<template v-else>
|
||||
<button type="button" class="tw-modal__cancel" @click="cancelExportTask(task)">取消</button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!exportTasks.length"><td colspan="8" class="tw-modal__empty">暂无导出任务</td></tr>
|
||||
@ -238,7 +240,15 @@ export default {
|
||||
this.exportTimer = window.setTimeout(() => this.refreshExportTasks(), 3000);
|
||||
},
|
||||
statusLabel(task) {
|
||||
return { queued: '排队中', running: '导出中', done: '成功', failed: '失败' }[task.status] || task.status;
|
||||
return { queued: '排队中', running: '导出中', done: '成功', failed: '失败', cancelled: '已取消' }[task.status] || task.status;
|
||||
},
|
||||
async cancelExportTask(task) {
|
||||
try {
|
||||
await window.axios.post(`${this.tasksBaseUrl}/${encodeURIComponent(task.token)}/cancel`);
|
||||
await this.refreshExportTasks();
|
||||
} catch (error) {
|
||||
this.exportHint = (error.response && error.response.data && error.response.data.message) || '取消失败,请重试。';
|
||||
}
|
||||
},
|
||||
downloadUrl(task) {
|
||||
return `${this.tasksBaseUrl}/${encodeURIComponent(task.token)}/download`;
|
||||
@ -266,6 +276,8 @@ export default {
|
||||
.tw-modal__table td { padding: .4rem .5rem; border-bottom: 1px solid #eeeae2; white-space: nowrap; }
|
||||
.tw-modal__empty { text-align: center; color: #8a8377; padding: 1.5rem !important; }
|
||||
.tw-modal__link { color: #856632; font-weight: 600; text-decoration: underline; }
|
||||
.tw-modal__cancel { border: 1px solid #d8d2c7; border-radius: .375rem; background: #fff; color: #b80000; font: 500 .75rem/1 inherit; padding: .25rem .5rem; cursor: pointer; }
|
||||
.tw-modal__cancel:hover { border-color: #b80000; }
|
||||
.tw-modal__foot { display: flex; justify-content: flex-end; gap: .5rem; padding: 1rem 1.25rem; border-top: 1px solid #eee7da; }
|
||||
.tw-export-status { padding: .0625rem .375rem; border-radius: .25rem; background: #f2f2f2; color: #595959; }
|
||||
.tw-export-status.is-done { background: #e6f0e0; color: #3f6b2b; }
|
||||
|
||||
@ -71,6 +71,8 @@ Route::get('ecommerce-finance/account-bills/export-tasks/data', [AccountBills::c
|
||||
->name('ecommerce-finance.account-bills.export-tasks.data');
|
||||
Route::get('ecommerce-finance/account-bills/export-tasks/{token}/download', [AccountBills::class, 'downloadExportTask'])
|
||||
->name('ecommerce-finance.account-bills.export-tasks.download');
|
||||
Route::post('ecommerce-finance/account-bills/export-tasks/{token}/cancel', [AccountBills::class, 'cancelExportTask'])
|
||||
->name('ecommerce-finance.account-bills.export-tasks.cancel');
|
||||
Route::get('ecommerce-finance/account-bills/{bill}', [AccountBills::class, 'show'])
|
||||
->name('ecommerce-finance.account-bills.show');
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user