2026-06-09 03:36:30 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
namespace app\common\service;
|
|
|
|
|
|
|
|
|
|
|
|
use think\Config;
|
|
|
|
|
|
use think\Db;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 工单云控同步相关系统配置读取
|
|
|
|
|
|
*/
|
|
|
|
|
|
class SplitSyncConfigService
|
|
|
|
|
|
{
|
|
|
|
|
|
private const DEFAULT_NODE_HOST = 'http://127.0.0.1:3001';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Node Headless 服务根地址
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function getNodeHost(): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$value = self::getConfigValue('split_scrm_node_host');
|
|
|
|
|
|
$value = trim($value);
|
|
|
|
|
|
return $value !== '' ? rtrim($value, '/') : self::DEFAULT_NODE_HOST;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 06:44:57 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 连续同步失败多少次后自动暂停工单(0 表示不因失败暂停)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function getFailPauseThreshold(): int
|
|
|
|
|
|
{
|
|
|
|
|
|
$value = self::getConfigValue('split_sync_fail_pause_threshold');
|
|
|
|
|
|
if ($value === '') {
|
|
|
|
|
|
return 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
return max(0, (int) $value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 03:36:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 指定工单类型的自动同步周期(分钟),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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 04:47:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 单次 cron 最多处理几条到期工单,避免一分钟内打满 Node Browser 槽位
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function getMaxTicketsPerCronRun(): int
|
|
|
|
|
|
{
|
|
|
|
|
|
$value = self::getConfigValue('split_sync_max_per_cron');
|
|
|
|
|
|
if ($value === '') {
|
2026-07-02 19:03:06 +08:00
|
|
|
|
return 5;
|
2026-06-20 04:47:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
return max(1, min(20, (int) $value));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 19:03:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 04:47:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 全局 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 19:03:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 后台投递 / 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 03:36:30 +08:00
|
|
|
|
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 : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|