74 lines
3.5 KiB
PHP
74 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Services;
|
|
|
|
use PDO;
|
|
use PDOException;
|
|
use SensitiveParameter;
|
|
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
|
|
|
class UpstreamConnection
|
|
{
|
|
/** @var array<string,PDO> */
|
|
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 配置。');
|
|
}
|
|
}
|
|
}
|