CaiWuXiTong/app/Console/Commands/RunAccountBillsExportTask.php

166 lines
7.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Modules\EcommerceFinance\AccountBillColumns;
use Modules\EcommerceFinance\JdBillPlatform;
use Modules\EcommerceFinance\Repositories\DewuSettlementBillRepository;
use Modules\EcommerceFinance\Repositories\JdDailyBillRepository;
use Modules\EcommerceFinance\Repositories\PinduoduoSettlementBillRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingDewuBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingJdWalletBillRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingPinduoduoBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\TaobaoWechatSettlementBillRepository;
use Modules\EcommerceFinance\Services\AccountBillsExportTasks;
use Modules\EcommerceFinance\Services\AccountLedgerRemarkMap;
use Throwable;
/**
* 后台执行「店铺收款账户账单」导出任务:生成 CSV 并更新任务状态,供页面列表下载。
*/
class RunAccountBillsExportTask extends Command
{
protected $signature = 'account-bills:export-run {token : 导出任务 token}';
protected $description = '执行店铺收款账户账单导出任务(后台进程调用)';
public function handle(AccountBillsExportTasks $tasks, AccountLedgerRemarkMap $remarkMap): int
{
$token = (string) $this->argument('token');
$task = $this->findTask($tasks, $token);
if ($task === null) {
$this->error('导出任务不存在:' . $token);
return self::FAILURE;
}
$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('导出任务没有可用字段。');
}
$csv = fopen($tasks->csvPath($task), 'w');
if ($csv === false) {
throw new \RuntimeException('无法创建导出文件。');
}
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, array_map(static fn (array $column): string => (string) ($column['label'] ?? ''), $columns));
$written = 0;
foreach ($this->rows($task, $remarkMap) as $row) {
fputcsv($csv, array_map(
static fn (array $column): string => self::cellValue($row, $column),
$columns,
));
$written++;
if ($written % 2000 === 0) {
// 用户在弹窗里点了「取消」:停止导出并删掉半成品文件。
$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;
}
$task = $tasks->patch($task, ['status' => 'running', 'rows' => $written]);
}
}
fclose($csv);
$task = $tasks->patch($task, [
'status' => 'done',
'rows' => $written,
'size' => (int) @filesize($tasks->csvPath($task)),
'finished_at' => AccountBillsExportTasks::beijingNow(),
'error' => null,
]);
$this->info('导出完成:' . $written . ' 行');
return self::SUCCESS;
} catch (Throwable $exception) {
$tasks->patch($task, [
'status' => 'failed',
'finished_at' => AccountBillsExportTasks::beijingNow(),
'error' => mb_substr($exception->getMessage(), 0, 200),
]);
$this->error('导出失败:' . $exception->getMessage());
return self::FAILURE;
}
}
private function findTask(AccountBillsExportTasks $tasks, string $token): ?array
{
$root = storage_path('app/account-bills-exports');
foreach (glob($root . '/*/' . $token . '.json') ?: [] as $path) {
$task = json_decode((string) file_get_contents($path), true);
if (is_array($task) && ($task['token'] ?? null) === $token) {
return $task;
}
}
return null;
}
/** @return iterable<int,array<string,mixed>> */
private function rows(array $task, AccountLedgerRemarkMap $remarkMap): iterable
{
$tab = (string) ($task['tab'] ?? '');
$filters = (array) ($task['filters'] ?? []);
// 后台导出固定按「记录编码」(数据库自增列)顺序取数:
// 整月按「发生时间」排序要先对上千万行排序,首批数据要几分钟,阶段进度长时间是 0;按自增列取数快很多。
$sort = 'record_code';
$dir = strtolower((string) ($filters['dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
unset($filters['sort'], $filters['dir'], $filters['export'], $filters['token'], $filters['total'], $filters['columns']);
$rows = match ($tab) {
'taobao-alipay' => app(StoreReceivingAlipayBillMirrorRepository::class)->exportRows($filters, $sort, $dir),
'taobao-wechat' => app(TaobaoWechatSettlementBillRepository::class)->exportAccountBills($filters, $sort, $dir),
'pinduoduo' => app(PinduoduoSettlementBillRepository::class)->exportAccountBills($filters, $sort, $dir),
'dewu' => app(DewuSettlementBillRepository::class)->exportAccountBills($filters, $sort, $dir),
'jd-transactions', 'jingxi' => JdBillPlatform::isJdBillTab($tab)
? app(JdDailyBillRepository::class)->forPlatform(JdBillPlatform::platformForBillType($tab))->exportRows($filters, $sort, $dir)
: [],
'jd-wallet' => app(StoreReceivingJdWalletBillRepository::class)->exportRows($filters, $sort, $dir),
'douyin' => app(StoreReceivingDouyinBillMirrorRepository::class)->exportRows($filters, $sort, $dir),
default => throw new \RuntimeException('该页签暂不支持后台导出:' . $tab),
};
return $remarkMap->decorateAll($rows, $tab);
}
/** @param array<string,mixed> $row */
private static function cellValue(array $row, array $column): string
{
$field = (string) ($column['field'] ?? '');
$value = str_starts_with($field, 'payload.')
? data_get($row['payload'] ?? [], substr($field, 8))
: data_get($row, $field);
$text = trim((string) ($value ?? ''));
if ($text === '') {
return '';
}
if (preg_match('/^\d{12,}$/', $text) === 1) {
return '="' . $text . '"';
}
return preg_match('/^[=+\-@]/u', $text) === 1 ? "'" . $text : $text;
}
}