*/ public static function getFailRetryMinutes(): array { $raw = trim(self::getConfigValue('split_sync_fail_retry_minutes')); if ($raw === '') { return []; } $parts = preg_split('/[,,\s]+/', $raw) ?: []; $minutes = []; foreach ($parts as $part) { $part = trim((string) $part); if ($part === '' || !ctype_digit($part)) { continue; } $minute = (int) $part; if ($minute < 1 || $minute > self::FAIL_RETRY_MINUTE_MAX) { continue; } $minutes[$minute] = $minute; } if ($minutes === []) { return []; } $list = array_values($minutes); sort($list, SORT_NUMERIC); return array_slice($list, 0, self::FAIL_RETRY_SCHEDULE_MAX); } /** * 是否配置了失败分阶段重试时间点 */ public static function hasFailRetrySchedule(): bool { return self::getFailRetryMinutes() !== []; } /** * 指定工单类型的自动同步周期(分钟),0 表示不自动同步 */ public static function getIntervalMinutes(string $ticketType): int { $ticketType = trim($ticketType); if ($ticketType === '') { return 0; } $key = 'split_sync_interval_' . $ticketType; $value = self::getConfigValue($key); return max(0, (int) $value); } /** * 单次 cron 最多处理几条到期工单,避免一分钟内打满 Node Browser 槽位 */ public static function getMaxTicketsPerCronRun(): int { $value = self::getConfigValue('split_sync_max_per_cron'); if ($value === '') { return 5; } return max(1, min(50, (int) $value)); } /** * Node 通道候选池倍数(候选数 = maxPerRun × 本值) */ public static function getNodeCandidatePoolMultiplier(): int { $value = self::getConfigValue('split_sync_node_candidate_multiplier'); if ($value === '') { return 10; } return max(3, min(50, (int) $value)); } /** * 全局 cron 锁过期秒数,防止异常退出后永久占锁 */ public static function getCronLockTtlSeconds(): int { $value = self::getConfigValue('split_sync_cron_lock_ttl'); if ($value === '') { return 1800; } return max(300, (int) $value); } /** * Node 排队数达到该阈值时,本轮 cron 不再提交新任务 */ public static function getNodeQueueSkipThreshold(): int { $value = self::getConfigValue('split_sync_node_queue_threshold'); if ($value === '') { return 2; } return max(0, (int) $value); } /** * 后台投递 / Cron 使用的 PHP CLI 可执行文件路径 * * Web(FPM) 环境下 PHP_BINARY 指向 php-fpm,不能用于 think 命令。 */ public static function getCliPhpBinary(): string { $configured = trim(self::getConfigValue('split_sync_cli_php')); if ($configured !== '' && self::isUsableCliPhp($configured)) { return $configured; } if (defined('PHP_BINDIR') && PHP_BINDIR !== '') { $candidate = rtrim(PHP_BINDIR, '/\\') . DIRECTORY_SEPARATOR . 'php'; if (self::isUsableCliPhp($candidate)) { return $candidate; } } if (PHP_SAPI === 'cli' && defined('PHP_BINARY') && PHP_BINARY !== '' && self::isUsableCliPhp(PHP_BINARY)) { return PHP_BINARY; } foreach (['/usr/bin/php', '/usr/local/bin/php'] as $candidate) { if (self::isUsableCliPhp($candidate)) { return $candidate; } } return 'php'; } /** * 判断路径是否为可用的 PHP CLI(排除 php-fpm) */ private static function isUsableCliPhp(string $path): bool { if (stripos($path, 'fpm') !== false) { return false; } if ($path === 'php') { return true; } return is_file($path) && is_executable($path); } private static function getConfigValue(string $name): string { $site = Config::get('site.' . $name); if ($site !== null && $site !== '') { return (string) $site; } $db = Db::name('config')->where('name', $name)->value('value'); return $db !== null ? (string) $db : ''; } }