CaiWuXiTong/modules/EcommerceFinance/Services/UpstreamConnection.php
aaron 93a27725b6 feat(财务台账): 接入 AI 轻量视图并改为分批增量快照 (#411)
本地验证:7 天和 30 天增量合并后,12,037 笔明细、收入 184,308,335.23、支出 155,917,513.13 保持不变。
2026-10-21 10:37:59 +08:00

74 lines
3.6 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, ?int $queryTimeout = null): PDO
{
$registry = app(UpstreamSourceRegistry::class);
if ($registry->driver($source) === 'sqlsrv') {
return UpstreamOdbcConnection::get($source, $queryTimeout);
}
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 配置。');
}
}
}