middleware(function ($request, $next) { abort_unless(SystemAccess::isSuperAdmin(), 403); return $next($request); }); } public function index( UpstreamStoredConfiguration $storedConfiguration, SystemDatabaseConfiguration $systemDatabaseConfiguration ) { $systemDatabase = $systemDatabaseConfiguration->current(); return view('system_settings.databases.index', [ 'connections' => $storedConfiguration->editable() ? DatabaseConnection::query() ->where('company_id', UpstreamStoredConfiguration::SYSTEM_COMPANY_ID) ->orderBy('name') ->get() : collect(), 'sourceLabels' => $this->sourceLabels(), 'systemDatabase' => [ 'connection' => $systemDatabase['connection'], 'driver' => $systemDatabase['driver'], 'host' => $systemDatabase['host'] ?: '本机文件', 'port' => $systemDatabase['port'], 'database' => $systemDatabase['database'], 'tls_required' => $systemDatabase['tls_required'], ], 'editable' => $storedConfiguration->editable(), ]); } public function editSystem(SystemDatabaseConfiguration $configuration) { $current = $configuration->current(); return view('system_settings.databases.system', [ 'values' => [ 'host' => $current['host'], 'port' => $current['port'], 'database' => $current['database'], 'username' => $current['username'], 'tls_required' => $current['tls_required'], ], ]); } public function updateSystem(Request $request, SystemDatabaseConfiguration $configuration) { $current = $configuration->current(); $data = $request->validate([ 'host' => ['required', 'string', 'max:191', 'not_regex:/[;\x00-\x1F]/'], 'port' => ['required', 'integer', 'between:1,65535'], 'database' => ['required', 'string', 'max:191', 'not_regex:/[;\x00-\x1F]/'], 'username' => ['required', 'string', 'max:191', 'not_regex:/[;\x00-\x1F]/'], 'password' => ['nullable', 'string', 'max:1000', 'not_regex:/[\r\n\x00]/'], 'tls_required' => ['nullable', 'boolean'], ]); $settings = [ 'driver' => 'mysql', 'host' => trim($data['host']), 'port' => (int) $data['port'], 'database' => trim($data['database']), 'username' => trim($data['username']), 'password' => $data['password'] !== null && $data['password'] !== '' ? $data['password'] : $current['password'], 'tls_required' => $request->boolean('tls_required'), ]; if ($settings['password'] === '') { throw ValidationException::withMessages([ 'password' => '当前没有可保留的密码,请填写数据库密码。', ]); } try { $result = $configuration->probe($settings); $configuration->persist($settings); } catch (\RuntimeException $exception) { throw ValidationException::withMessages([ 'connection' => $exception->getMessage(), ]); } flash(sprintf( '系统数据库连接已验证并保存,共检测到 %d 个用户;新配置将在下一次请求生效。', $result['user_count'] ))->success(); return redirect()->route('system-settings.databases.index'); } public function create(UpstreamStoredConfiguration $storedConfiguration) { abort_unless($storedConfiguration->editable(), 403); return view('system_settings.databases.form', [ 'connection' => null, 'sourceLabels' => $this->sourceLabels(), 'values' => $this->defaultValues(), ]); } public function store(Request $request, UpstreamStoredConfiguration $storedConfiguration) { abort_unless($storedConfiguration->editable(), 403); $data = $this->validateConnection($request); $connection = DatabaseConnection::create($this->attributes($data, null, $request)); $this->resetSource($connection->source_key, $storedConfiguration); flash('数据库配置已新增。')->success(); return redirect()->route('system-settings.databases.index'); } public function edit(int $connection, UpstreamStoredConfiguration $storedConfiguration) { abort_unless($storedConfiguration->editable(), 403); $connection = $this->connection($connection); return view('system_settings.databases.form', [ 'connection' => $connection, 'sourceLabels' => $this->sourceLabels(), 'values' => $this->values($connection), ]); } public function update(int $connection, Request $request, UpstreamStoredConfiguration $storedConfiguration) { abort_unless($storedConfiguration->editable(), 403); $connection = $this->connection($connection); $oldSource = $connection->source_key; $data = $this->validateConnection($request, $connection); $connection->update($this->attributes($data, $connection, $request)); $this->resetSource($oldSource, $storedConfiguration); $this->resetSource($connection->source_key, $storedConfiguration); flash('数据库配置已修改。')->success(); return redirect()->route('system-settings.databases.index'); } public function destroy(int $connection, UpstreamStoredConfiguration $storedConfiguration) { abort_unless($storedConfiguration->editable(), 403); $connection = $this->connection($connection); $source = $connection->source_key; $connection->delete(); $this->resetSource($source, $storedConfiguration); flash('数据库配置已删除。')->success(); return redirect()->route('system-settings.databases.index'); } public function test(int $connection, UpstreamStoredConfiguration $storedConfiguration) { abort_unless($storedConfiguration->editable(), 403); $connection = $this->connection($connection); try { UpstreamOdbcConnection::probeConfiguration($this->odbcConfiguration($connection), $connection->name); } catch (ServiceUnavailableHttpException) { throw ValidationException::withMessages([ 'connection' => '连接失败。请检查地址、端口、数据库、账号、网络白名单和本机 ODBC 驱动。', ]); } flash($connection->name . '连接成功,仅执行了 SELECT 1 只读测试。')->success(); return redirect()->route('system-settings.databases.index'); } private function validateConnection(Request $request, ?DatabaseConnection $connection = null): array { $sourceKeys = array_keys($this->sourceLabels()); return $request->validate([ 'name' => [ 'required', 'string', 'max:191', Rule::unique('system_database_connections', 'name') ->where(fn ($query) => $query->where('company_id', UpstreamStoredConfiguration::SYSTEM_COMPANY_ID)) ->ignore($connection?->id), ], 'source_key' => [ 'nullable', Rule::in($sourceKeys), Rule::unique('system_database_connections', 'source_key') ->where(fn ($query) => $query->where('company_id', UpstreamStoredConfiguration::SYSTEM_COMPANY_ID)) ->ignore($connection?->id), ], 'host' => ['required', 'string', 'max:191', 'not_regex:/[;\x00-\x1F]/'], 'port' => ['required', 'integer', 'between:1,65535'], 'database_name' => ['required', 'string', 'max:191', 'not_regex:/[;\x00-\x1F]/'], 'username' => ['required', 'string', 'max:191', 'not_regex:/[;\x00-\x1F]/'], 'password' => [$connection ? 'nullable' : 'required', 'string', 'max:1000'], 'odbc_driver' => ['required', 'string', 'max:128', 'not_regex:/[;{}\x00-\x1F]/'], 'tds_version' => ['required', 'regex:/^\d+(?:\.\d+)?$/', 'max:16'], 'connect_timeout' => ['required', 'integer', 'between:1,30'], 'query_timeout' => ['required', 'integer', 'between:1,120'], 'allowed_scopes' => ['nullable', 'string', 'max:4000'], 'enabled' => ['nullable', 'boolean'], ]); } private function attributes(array $data, ?DatabaseConnection $connection, Request $request): array { $attributes = [ 'company_id' => UpstreamStoredConfiguration::SYSTEM_COMPANY_ID, 'source_key' => $data['source_key'] ?? null, 'name' => trim($data['name']), 'driver' => 'sqlsrv', 'host' => trim($data['host']), 'port' => (int) $data['port'], 'database_name' => trim($data['database_name']), 'username' => trim($data['username']), 'odbc_driver' => trim($data['odbc_driver']), 'tds_version' => trim($data['tds_version']), 'connect_timeout' => (int) $data['connect_timeout'], 'query_timeout' => (int) $data['query_timeout'], 'allowed_scopes' => $this->parseScopes((string) ($data['allowed_scopes'] ?? '')), 'charset' => 'UTF-8', 'enabled' => $request->boolean('enabled'), ]; if (! empty($data['password'])) { $attributes['password'] = $data['password']; } return $attributes; } private function parseScopes(string $value): array { $scopes = array_values(array_unique(array_filter(array_map('trim', preg_split('/[,\r\n]+/', $value) ?: [])))); foreach ($scopes as $scope) { if ($scope === '*' || preg_match('/^[a-z0-9_.-]+(?:\.\*)?$/i', $scope) !== 1) { throw ValidationException::withMessages([ 'allowed_scopes' => '数据范围只允许精确名称或“前缀.*”,不能使用全局 *。', ]); } } return $scopes; } private function connection(int $id): DatabaseConnection { return DatabaseConnection::query() ->where('company_id', UpstreamStoredConfiguration::SYSTEM_COMPANY_ID) ->findOrFail($id); } private function sourceLabels(): array { return collect((array) config('upstreams.sources', [])) ->mapWithKeys(fn (array $source, string $key) => [$key => $source['label'] ?? $key]) ->all(); } private function defaultValues(): array { return [ 'name' => '', 'source_key' => '', 'enabled' => false, 'host' => '', 'port' => 1433, 'database_name' => '', 'username' => '', 'odbc_driver' => 'FreeTDS', 'tds_version' => '7.4', 'connect_timeout' => 5, 'query_timeout' => 15, 'allowed_scopes' => '', ]; } private function values(DatabaseConnection $connection): array { return [ 'name' => $connection->name, 'source_key' => $connection->source_key, 'enabled' => $connection->enabled, 'host' => $connection->host, 'port' => $connection->port, 'database_name' => $connection->database_name, 'username' => $connection->username, 'odbc_driver' => $connection->odbc_driver, 'tds_version' => $connection->tds_version, 'connect_timeout' => $connection->connect_timeout, 'query_timeout' => $connection->query_timeout, 'allowed_scopes' => implode(', ', $connection->allowed_scopes ?? []), ]; } private function odbcConfiguration(DatabaseConnection $connection): array { return [ 'host' => $connection->host, 'port' => $connection->port, 'database' => $connection->database_name, 'username' => $connection->username, 'password' => $connection->password, 'odbc_driver' => $connection->odbc_driver, 'tds_version' => $connection->tds_version, 'connect_timeout' => $connection->connect_timeout, 'query_timeout' => $connection->query_timeout, ]; } private function resetSource(?string $source, UpstreamStoredConfiguration $storedConfiguration): void { if (! $source) { return; } $storedConfiguration->forget($source); UpstreamOdbcConnection::reset($source); } }