提交同步状态为异步
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
/**
|
||||
* 工单手工同步 CLI 后台投递
|
||||
*
|
||||
* Web 请求仅负责鉴权与投递,实际 Spider 在独立 CLI 进程中执行,
|
||||
* 避免长时间占用 PHP-FPM 与 Session 锁导致后台其它页面无法打开。
|
||||
*/
|
||||
class SplitTicketSyncDispatchService
|
||||
{
|
||||
/**
|
||||
* 投递手工同步任务到后台 CLI
|
||||
*
|
||||
* @param int[] $ticketIds 已通过权限校验的工单 ID
|
||||
* @return array{queued:int[],skipped:int[],failed:int[]}
|
||||
*/
|
||||
public function dispatchManual(array $ticketIds): array
|
||||
{
|
||||
$queued = [];
|
||||
$skipped = [];
|
||||
$failed = [];
|
||||
|
||||
$php = $this->resolvePhpBinary();
|
||||
$think = $this->resolveThinkScript();
|
||||
$root = $this->resolveRootPath();
|
||||
$lockService = new SplitTicketSyncLockService();
|
||||
|
||||
foreach ($ticketIds as $rawId) {
|
||||
$ticketId = (int) $rawId;
|
||||
if ($ticketId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($lockService->isLocked($ticketId)) {
|
||||
$skipped[] = $ticketId;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->spawnCli($php, $think, $root, $ticketId)) {
|
||||
$queued[] = $ticketId;
|
||||
} else {
|
||||
$failed[] = $ticketId;
|
||||
}
|
||||
}
|
||||
|
||||
SplitTicketSyncLogger::log('web', 'manual sync dispatched', [
|
||||
'queued' => $queued,
|
||||
'skipped' => $skipped,
|
||||
'failed' => $failed,
|
||||
]);
|
||||
|
||||
return [
|
||||
'queued' => $queued,
|
||||
'skipped' => $skipped,
|
||||
'failed' => $failed,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仍在同步中的工单 ID(依据 runtime 文件锁)
|
||||
*
|
||||
* @param int[] $ticketIds
|
||||
* @return int[]
|
||||
*/
|
||||
public function filterSyncingIds(array $ticketIds): array
|
||||
{
|
||||
$lockService = new SplitTicketSyncLockService();
|
||||
$syncing = [];
|
||||
foreach ($ticketIds as $rawId) {
|
||||
$ticketId = (int) $rawId;
|
||||
if ($ticketId > 0 && $lockService->isLocked($ticketId)) {
|
||||
$syncing[] = $ticketId;
|
||||
}
|
||||
}
|
||||
|
||||
return $syncing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台启动 split:sync-tickets CLI
|
||||
*/
|
||||
private function spawnCli(string $php, string $think, string $root, int $ticketId): bool
|
||||
{
|
||||
$logDir = $this->resolveLogDir();
|
||||
$logFile = $logDir . 'cli_' . $ticketId . '_' . date('YmdHis') . '.log';
|
||||
|
||||
$inner = sprintf(
|
||||
'%s %s split:sync-tickets --ticket=%d >> %s 2>&1',
|
||||
escapeshellarg($php),
|
||||
escapeshellarg($think),
|
||||
$ticketId,
|
||||
escapeshellarg($logFile)
|
||||
);
|
||||
|
||||
if ($this->isWindows()) {
|
||||
$command = sprintf('start /B cmd /C %s', $inner);
|
||||
} else {
|
||||
$command = sprintf('cd %s && nohup %s &', escapeshellarg($root), $inner);
|
||||
}
|
||||
|
||||
if ($this->canUseExec()) {
|
||||
@exec($command, $output, $exitCode);
|
||||
SplitTicketSyncLogger::log('web', 'cli spawn exec', [
|
||||
'ticketId' => $ticketId,
|
||||
'command' => $command,
|
||||
'exitCode' => $exitCode,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->canUseShellExec()) {
|
||||
@shell_exec($command);
|
||||
SplitTicketSyncLogger::log('web', 'cli spawn shell_exec', [
|
||||
'ticketId' => $ticketId,
|
||||
'command' => $command,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
SplitTicketSyncLogger::log('web', 'cli spawn failed: exec disabled', [
|
||||
'ticketId' => $ticketId,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function resolvePhpBinary(): string
|
||||
{
|
||||
if (defined('PHP_BINARY') && PHP_BINARY !== '') {
|
||||
return PHP_BINARY;
|
||||
}
|
||||
|
||||
return 'php';
|
||||
}
|
||||
|
||||
private function resolveThinkScript(): string
|
||||
{
|
||||
$root = $this->resolveRootPath();
|
||||
return rtrim($root, '/\\') . DIRECTORY_SEPARATOR . 'think';
|
||||
}
|
||||
|
||||
private function resolveRootPath(): string
|
||||
{
|
||||
if (defined('ROOT_PATH') && ROOT_PATH !== '') {
|
||||
return rtrim(ROOT_PATH, '/\\') . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
return rtrim(dirname(__DIR__, 3), '/\\') . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
private function resolveLogDir(): string
|
||||
{
|
||||
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : ($this->resolveRootPath() . 'runtime' . DIRECTORY_SEPARATOR);
|
||||
$dir = rtrim($runtime, '/\\') . DIRECTORY_SEPARATOR . 'split_ticket_sync' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
return $dir;
|
||||
}
|
||||
|
||||
private function isWindows(): bool
|
||||
{
|
||||
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function disabledFunctions(): array
|
||||
{
|
||||
$raw = (string) ini_get('disable_functions');
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(array_map('trim', explode(',', $raw)));
|
||||
}
|
||||
|
||||
private function canUseExec(): bool
|
||||
{
|
||||
return function_exists('exec') && !in_array('exec', $this->disabledFunctions(), true);
|
||||
}
|
||||
|
||||
private function canUseShellExec(): bool
|
||||
{
|
||||
return function_exists('shell_exec') && !in_array('shell_exec', $this->disabledFunctions(), true);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,20 @@ class SplitTicketSyncLockService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单是否处于同步中(锁文件存在且未过期)
|
||||
*/
|
||||
public function isLocked(int $ticketId): bool
|
||||
{
|
||||
$path = $this->lockPath($ticketId);
|
||||
if ($this->isStaleLock($path)) {
|
||||
@unlink($path);
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_file($path);
|
||||
}
|
||||
|
||||
private function lockPath(int $ticketId): string
|
||||
{
|
||||
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
||||
|
||||
Reference in New Issue
Block a user