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');