*/ private static array $native = []; public static function get(string $source): PDO { $registry = app(UpstreamSourceRegistry::class); if ($registry->driver($source) === 'sqlsrv') { return UpstreamOdbcConnection::get($source); } if (! $registry->enabled($source)) { throw new ServiceUnavailableHttpException(30, '该数据源尚未启用。'); } return self::$native[$source] ??= self::openNative($registry->driver($source), $registry->value($source, 'connection', [])); } public static function reset(?string $source = null): void { if ($source === null) self::$native = []; else unset(self::$native[$source]); UpstreamOdbcConnection::reset($source); } public static function openNative(string $driver, #[SensitiveParameter] array $config): PDO { if (! in_array($driver, ['pgsql', 'mysql'], true) || ! in_array($driver, PDO::getAvailableDrivers(), true)) { throw new ServiceUnavailableHttpException(30, '应用缺少该数据库类型的驱动,请更新应用镜像。'); } $timeout = min(max((int) ($config['connect_timeout'] ?? 5), 1), 30); $seconds = min(max((int) ($config['query_timeout'] ?? 15), 1), 15); $options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_PERSISTENT => false, PDO::ATTR_TIMEOUT => $timeout]; $host = (string) $config['host']; $database = (string) $config['database']; if (preg_match('/[;\x00-\x1F]/', $host . $database)) { throw new ServiceUnavailableHttpException(30, '主机或数据库名称包含不支持的字符。'); } $port = min(max((int) $config['port'], 1), 65535); if ($driver === 'pgsql') { $quote = static fn (string $s): string => "'" . str_replace(['\\', "'"], ['\\\\', "\\'"], $s) . "'"; $dsn = 'pgsql:host=' . $quote($host) . ';port=' . $port . ';dbname=' . $quote($database) . ';sslmode=require;connect_timeout=' . $timeout . ";options='-c default_transaction_read_only=on -c statement_timeout=" . ($seconds * 1000) . "'"; } else { $dsn = "mysql:host={$host};port={$port};dbname={$database};charset=utf8mb4"; $options[PDO::MYSQL_ATTR_SSL_CIPHER] = 'DEFAULT'; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; } try { $pdo = new PDO($dsn, (string) $config['username'], (string) $config['password'], $options); if ($driver === 'mysql') { $tls = $pdo->query("SHOW SESSION STATUS LIKE 'Ssl_cipher'")->fetch(PDO::FETCH_NUM); if (empty($tls[1])) throw new ServiceUnavailableHttpException(30, '上游数据库未建立 TLS 加密连接。'); $pdo->exec('SET SESSION TRANSACTION READ ONLY'); $pdo->exec(str_contains($pdo->getAttribute(PDO::ATTR_SERVER_VERSION), 'MariaDB') ? 'SET SESSION max_statement_time=' . $seconds : 'SET SESSION MAX_EXECUTION_TIME=' . ($seconds * 1000)); } return $pdo; } catch (PDOException) { throw new ServiceUnavailableHttpException(30, '上游只读连接失败,请检查账号、权限、网络和 TLS 配置。'); } } }