diff --git a/app/Console/Commands/RunAccountBillsExportTask.php b/app/Console/Commands/RunAccountBillsExportTask.php
index 8c44d253..a18bf71a 100644
--- a/app/Console/Commands/RunAccountBillsExportTask.php
+++ b/app/Console/Commands/RunAccountBillsExportTask.php
@@ -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);
diff --git a/modules/EcommerceFinance/Http/Controllers/AccountBills.php b/modules/EcommerceFinance/Http/Controllers/AccountBills.php
index 89dd2a58..a3361f78 100644
--- a/modules/EcommerceFinance/Http/Controllers/AccountBills.php
+++ b/modules/EcommerceFinance/Http/Controllers/AccountBills.php
@@ -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。
*
diff --git a/modules/EcommerceFinance/Services/AccountBillsExportTasks.php b/modules/EcommerceFinance/Services/AccountBillsExportTasks.php
index 5beb8ce1..7c1b2048 100644
--- a/modules/EcommerceFinance/Services/AccountBillsExportTasks.php
+++ b/modules/EcommerceFinance/Services/AccountBillsExportTasks.php
@@ -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,
diff --git a/resources/assets/js/components/business/AccountBills.vue b/resources/assets/js/components/business/AccountBills.vue
index 679b2f56..83369f15 100644
--- a/resources/assets/js/components/business/AccountBills.vue
+++ b/resources/assets/js/components/business/AccountBills.vue
@@ -82,7 +82,9 @@
下载
{{ task.error || '失败' }}
- —
+
+
+
|
| 暂无导出任务 |
@@ -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; }
diff --git a/resources/assets/js/components/business/TaobaoAlipayBills.vue b/resources/assets/js/components/business/TaobaoAlipayBills.vue
index 7411b4ae..e2056dc4 100644
--- a/resources/assets/js/components/business/TaobaoAlipayBills.vue
+++ b/resources/assets/js/components/business/TaobaoAlipayBills.vue
@@ -90,7 +90,9 @@
下载
{{ task.error || '失败' }}
- —
+
+
+
|
| 暂无导出任务 |
@@ -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; }
diff --git a/routes/admin.php b/routes/admin.php
index 9a6cb2fc..bc4d7e4b 100644
--- a/routes/admin.php
+++ b/routes/admin.php
@@ -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');