CaiWuXiTong/modules/EcommerceFinance/Services/AccountBillsExportTasks.php

178 lines
5.7 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 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,页面会提示重试。
}
}
}