178 lines
5.7 KiB
PHP
178 lines
5.7 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Str;
|
||
use RuntimeException;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 店铺收款账户账单的「导出任务」:后台生成 CSV 落盘,页面按任务列表查看状态并下载。
|
||
*
|
||
* 任务与文件都存在 storage/app/account-bills-exports/{userId}/ 下(JSON 记录状态 + CSV 文件),
|
||
* 不依赖浏览器保持连接,也不需要队列表:请求里用 detach 的 php-cli 进程跑导出。
|
||
*/
|
||
class AccountBillsExportTasks
|
||
{
|
||
/** 任务类型(列表里的「类型」列)。 */
|
||
public const TYPE_LABEL = '平台原始账单导出';
|
||
|
||
public function baseDir(int $userId): string
|
||
{
|
||
return storage_path('app/account-bills-exports/' . $userId);
|
||
}
|
||
|
||
public function jsonPath(array $task): string
|
||
{
|
||
return $this->baseDir((int) $task['user_id']) . '/' . $task['token'] . '.json';
|
||
}
|
||
|
||
public function csvPath(array $task): string
|
||
{
|
||
return $this->baseDir((int) $task['user_id']) . '/' . $task['token'] . '.csv';
|
||
}
|
||
|
||
/**
|
||
* 新建任务(只落状态,真正的导出由后台进程执行)。
|
||
*
|
||
* @param array<string,mixed> $filters
|
||
* @param list<array{field:string,label:string,kind?:string}> $columns
|
||
*/
|
||
public function create(int $userId, string $tab, string $tabLabel, array $filters, array $columns, int $total): array
|
||
{
|
||
$task = [
|
||
'token' => (string) Str::uuid(),
|
||
'user_id' => $userId,
|
||
'tab' => $tab,
|
||
'tab_label' => $tabLabel,
|
||
'type' => self::TYPE_LABEL,
|
||
'status' => 'queued',
|
||
'rows' => 0,
|
||
'total' => max(0, $total),
|
||
'filters' => $filters,
|
||
'columns' => $columns,
|
||
'error' => null,
|
||
'planned_at' => Carbon::now()->toDateTimeString(),
|
||
'started_at' => null,
|
||
'finished_at' => null,
|
||
'size' => 0,
|
||
];
|
||
|
||
$this->write($task);
|
||
|
||
return $task;
|
||
}
|
||
|
||
public function write(array $task): void
|
||
{
|
||
$dir = $this->baseDir((int) $task['user_id']);
|
||
if (! is_dir($dir) && ! mkdir($dir, 0775, true) && ! is_dir($dir)) {
|
||
throw new RuntimeException('无法创建导出目录:' . $dir);
|
||
}
|
||
|
||
file_put_contents($this->jsonPath($task), json_encode($task, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||
}
|
||
|
||
public function patch(array $task, array $patch): array
|
||
{
|
||
$task = array_merge($task, $patch);
|
||
$this->write($task);
|
||
|
||
return $task;
|
||
}
|
||
|
||
public function find(int $userId, string $token): ?array
|
||
{
|
||
if (preg_match('/^[A-Za-z0-9_-]{8,64}$/', $token) !== 1) {
|
||
return null;
|
||
}
|
||
|
||
$path = $this->baseDir($userId) . '/' . $token . '.json';
|
||
if (! is_file($path)) {
|
||
return null;
|
||
}
|
||
|
||
$task = json_decode((string) file_get_contents($path), true);
|
||
|
||
return is_array($task) ? $task : null;
|
||
}
|
||
|
||
/**
|
||
* 当前用户的导出任务,按计划时间倒序。
|
||
*
|
||
* @return list<array<string,mixed>>
|
||
*/
|
||
public function all(int $userId, int $limit = 50): array
|
||
{
|
||
$dir = $this->baseDir($userId);
|
||
if (! is_dir($dir)) {
|
||
return [];
|
||
}
|
||
|
||
$tasks = [];
|
||
$expireBefore = Carbon::now()->subDays(7)->getTimestamp();
|
||
foreach (glob($dir . '/*.json') ?: [] as $path) {
|
||
$task = json_decode((string) file_get_contents($path), true);
|
||
if (! is_array($task) || ! isset($task['token'])) {
|
||
continue;
|
||
}
|
||
// 只保留最近 7 天,避免任务文件无限堆积。
|
||
if (($task['status'] ?? '') !== 'running' && filemtime($path) !== false && filemtime($path) < $expireBefore) {
|
||
@unlink($path);
|
||
$csv = $dir . '/' . $task['token'] . '.csv';
|
||
if (is_file($csv)) {
|
||
@unlink($csv);
|
||
}
|
||
continue;
|
||
}
|
||
$tasks[] = $task;
|
||
}
|
||
|
||
usort($tasks, static fn (array $a, array $b): int => strcmp((string) ($b['planned_at'] ?? ''), (string) ($a['planned_at'] ?? '')));
|
||
|
||
return array_slice($tasks, 0, $limit);
|
||
}
|
||
|
||
/** 列表页展示用的字段(去掉 filters/columns 这类大字段)。 */
|
||
public function present(array $task): array
|
||
{
|
||
return [
|
||
'token' => $task['token'],
|
||
'type' => $task['type'] ?? self::TYPE_LABEL,
|
||
'tab' => $task['tab'] ?? '',
|
||
'tab_label' => $task['tab_label'] ?? '',
|
||
'status' => $task['status'] ?? 'queued',
|
||
'rows' => (int) ($task['rows'] ?? 0),
|
||
'total' => (int) ($task['total'] ?? 0),
|
||
'size' => (int) ($task['size'] ?? 0),
|
||
'error' => $task['error'] ?? null,
|
||
'planned_at' => $task['planned_at'] ?? null,
|
||
'started_at' => $task['started_at'] ?? null,
|
||
'finished_at' => $task['finished_at'] ?? null,
|
||
];
|
||
}
|
||
|
||
/** 后台执行导出(脱离当前请求)。 */
|
||
public function dispatch(array $task): void
|
||
{
|
||
$php = PHP_BINDIR . '/php';
|
||
if (! is_file($php)) {
|
||
$php = 'php';
|
||
}
|
||
|
||
$artisan = base_path('artisan');
|
||
$command = escapeshellcmd($php) . ' ' . escapeshellarg($artisan)
|
||
. ' account-bills:export-run ' . escapeshellarg((string) $task['token'])
|
||
. ' > /dev/null 2>&1 &';
|
||
|
||
try {
|
||
if (function_exists('exec')) {
|
||
exec($command);
|
||
}
|
||
} catch (Throwable) {
|
||
// 起不了后台进程时保持 queued,页面会提示重试。
|
||
}
|
||
}
|
||
}
|