request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } try { // 获取数据库配置 $config = Config::get('database.connections.mysql'); $database = $config['database'] ?? ''; // 获取数据库大小 $dbSize = $this->getDatabaseSize($database); // 获取表数量 $tableCount = $this->getTableCount($database); // 获取连接状态 try { Db::query('SELECT 1'); $connected = true; } catch (\Exception $e) { $connected = false; } return success([ 'databaseType' => 'MySQL', 'databaseName' => $database, 'connected' => $connected, 'databaseSize' => $dbSize, 'tableCount' => $tableCount ]); } catch (\Exception $e) { return error('获取数据库信息失败:' . $e->getMessage(), 500); } } /** * 获取表列表 * @return \think\response\Json */ public function tables() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } try { $config = Config::get('database.connections.mysql'); $database = $config['database'] ?? ''; $prefix = $config['prefix'] ?? 'mbti_'; // 获取所有表 $tables = Db::query("SHOW TABLE STATUS FROM `{$database}`"); $result = []; foreach ($tables as $table) { $tableName = $table['Name']; // 只显示带前缀的表(或者所有表) if (empty($prefix) || strpos($tableName, $prefix) === 0) { // 获取记录数 $rowCount = Db::query("SELECT COUNT(*) as count FROM `{$tableName}`")[0]['count'] ?? 0; // 获取索引数 $indexes = Db::query("SHOW INDEX FROM `{$tableName}`"); $indexCount = count(array_unique(array_column($indexes, 'Key_name'))); $result[] = [ 'name' => $tableName, 'docCount' => intval($rowCount), 'size' => intval($table['Data_length'] + $table['Index_length']), 'indexCount' => $indexCount, 'engine' => $table['Engine'] ?? '', 'collation' => $table['Collation'] ?? '' ]; } } return success($result); } catch (\Exception $e) { return error('获取表列表失败:' . $e->getMessage(), 500); } } /** * 查看表数据 * @return \think\response\Json */ public function viewTable() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } $tableName = Request::param('table', ''); $page = Request::param('page', 1); $pageSize = Request::param('pageSize', 20); if (empty($tableName)) { return error('表名不能为空', 400); } try { // 验证表是否存在 $config = Config::get('database.connections.mysql'); $database = $config['database'] ?? ''; $tables = Db::query("SHOW TABLES FROM `{$database}` LIKE '{$tableName}'"); if (empty($tables)) { return error('表不存在', 404); } // 获取表结构 $columns = Db::query("SHOW COLUMNS FROM `{$tableName}`"); // 获取数据 $total = Db::name(str_replace($config['prefix'] ?? 'mbti_', '', $tableName))->count(); $list = Db::name(str_replace($config['prefix'] ?? 'mbti_', '', $tableName)) ->page($page, $pageSize) ->select() ->toArray(); return success([ 'columns' => $columns, 'list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize ]); } catch (\Exception $e) { return error('查看表数据失败:' . $e->getMessage(), 500); } } /** * 导出表数据 * @return \think\response\Json */ public function exportTable() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } $tableName = Request::param('table', ''); if (empty($tableName)) { return error('表名不能为空', 400); } try { // 生成SQL导出文件 $backupDir = root_path() . 'runtime/backup/'; if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } $filename = $tableName . '_' . date('YmdHis') . '.sql'; $filepath = $backupDir . $filename; $this->exportTableToSql($tableName, $filepath); return success([ 'filename' => $filename, 'filepath' => $filepath, 'downloadUrl' => '/api/v1/superadmin/database/download?file=' . urlencode($filename) ], '导出成功'); } catch (\Exception $e) { return error('导出表数据失败:' . $e->getMessage(), 500); } } /** * 清空表数据 * @return \think\response\Json */ public function clearTable() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } $tableName = Request::param('table', ''); if (empty($tableName)) { return error('表名不能为空', 400); } try { // 验证表是否存在 $config = Config::get('database.connections.mysql'); $database = $config['database'] ?? ''; $tables = Db::query("SHOW TABLES FROM `{$database}` LIKE '{$tableName}'"); if (empty($tables)) { return error('表不存在', 404); } // 清空表 Db::execute("TRUNCATE TABLE `{$tableName}`"); return success(null, '表数据已清空'); } catch (\Exception $e) { return error('清空表数据失败:' . $e->getMessage(), 500); } } /** * 备份数据库 * @return \think\response\Json */ public function backup() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } try { $config = Config::get('database.connections.mysql'); $host = $config['hostname'] ?? 'localhost'; $port = $config['hostport'] ?? 3306; $database = $config['database'] ?? ''; $username = $config['username'] ?? ''; $password = $config['password'] ?? ''; // 创建备份目录 $backupDir = root_path() . 'runtime/backup/'; if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } $filename = 'backup_' . $database . '_' . date('YmdHis') . '.sql'; $filepath = $backupDir . $filename; // 优先使用PHP方式备份(更可靠) $this->backupDatabase($database, $filepath); // 如果文件不存在或为空,尝试使用mysqldump if (!file_exists($filepath) || filesize($filepath) == 0) { $mysqldumpPath = $this->findMysqldump(); if ($mysqldumpPath) { // 使用mysqldump命令 $command = sprintf( '"%s" -h%s -P%s -u%s -p%s %s > "%s" 2>&1', $mysqldumpPath, escapeshellarg($host), escapeshellarg($port), escapeshellarg($username), escapeshellarg($password), escapeshellarg($database), escapeshellarg($filepath) ); exec($command, $output, $returnVar); if ($returnVar !== 0) { throw new \Exception('mysqldump执行失败: ' . implode("\n", $output)); } } } // 获取文件大小 $fileSize = filesize($filepath); // 上传到OSS $ossUrl = null; $ossPath = null; try { $ossResult = $this->uploadBackupToOss($filepath, $filename); if ($ossResult) { $ossUrl = $ossResult['url']; $ossPath = $ossResult['path']; } } catch (\Exception $e) { // OSS上传失败不影响备份成功,只记录错误 Log::error('备份文件上传OSS失败:' . $e->getMessage()); } // 记录备份信息 $this->saveBackupRecord($filename, $filepath, $fileSize, $ossUrl, $ossPath); return success([ 'filename' => $filename, 'filepath' => $filepath, 'size' => $fileSize, 'time' => date('Y-m-d H:i:s'), 'ossUrl' => $ossUrl, 'ossPath' => $ossPath, 'downloadUrl' => '/api/v1/superadmin/database/download?file=' . urlencode($filename) ], '备份成功' . ($ossUrl ? ',已上传到OSS' : '')); } catch (\Exception $e) { return error('备份失败:' . $e->getMessage(), 500); } } /** * 获取备份记录列表 * @return \think\response\Json */ public function backups() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } try { // 从数据库读取备份记录 $records = BackupRecordModel::order('createdAt', 'desc')->select()->toArray(); $backups = []; foreach ($records as $record) { $backups[] = [ 'id' => $record['id'], 'filename' => $record['filename'], 'time' => date('Y-m-d\TH:i:s', $record['createdAt']), 'size' => intval($record['fileSize']), 'status' => $record['status'] ?? 'success', 'ossUrl' => $record['ossUrl'] ?? null, 'ossPath' => $record['ossPath'] ?? null, 'filepath' => $record['filepath'] ?? null ]; } return success($backups); } catch (\Exception $e) { return error('获取备份记录失败:' . $e->getMessage(), 500); } } /** * 删除备份记录(软删除) * @return \think\response\Json */ public function delete() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } // 支持从路由参数或请求参数获取ID $id = Request::param('id', 0) ?: Request::route('id', 0); if (empty($id)) { return error('记录ID不能为空', 400); } try { $record = BackupRecordModel::find($id); if (!$record) { return error('备份记录不存在', 404); } // 软删除(ThinkPHP的SoftDelete会自动设置deletedAt) $record->delete(); return success(null, '备份记录已删除'); } catch (\Exception $e) { return error('删除失败:' . $e->getMessage(), 500); } } /** * 下载备份文件 */ public function download() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } $filename = Request::param('file', ''); if (empty($filename)) { return error('文件名不能为空', 400); } // 安全检查:只允许下载备份目录下的文件 $backupDir = root_path() . 'runtime/backup/'; $filepath = realpath($backupDir . $filename); if (!$filepath || strpos($filepath, realpath($backupDir)) !== 0) { return error('文件不存在', 404); } if (!file_exists($filepath)) { return error('文件不存在', 404); } // 下载文件 header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . filesize($filepath)); readfile($filepath); exit; } /** * 恢复数据库 * @return \think\response\Json */ public function restore() { // 验证是否为超级管理员 $user = $this->request->user ?? null; if (!$user || $user['role'] !== 'superadmin') { return error('无权限访问', 403); } $filename = Request::param('file', ''); if (empty($filename)) { return error('文件名不能为空', 400); } try { $backupDir = root_path() . 'runtime/backup/'; $filepath = realpath($backupDir . $filename); if (!$filepath || strpos($filepath, realpath($backupDir)) !== 0) { return error('文件不存在', 404); } if (!file_exists($filepath)) { return error('文件不存在', 404); } $config = Config::get('database.connections.mysql'); $host = $config['hostname'] ?? 'localhost'; $port = $config['hostport'] ?? 3306; $database = $config['database'] ?? ''; $username = $config['username'] ?? ''; $password = $config['password'] ?? ''; // 使用PHP方式恢复 $this->restoreDatabase($filepath); return success(null, '数据库恢复成功'); } catch (\Exception $e) { return error('恢复失败:' . $e->getMessage(), 500); } } /** * 获取数据库大小 */ private function getDatabaseSize($database) { try { $result = Db::query("SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = '{$database}'"); return floatval($result[0]['size_mb'] ?? 0); } catch (\Exception $e) { return 0; } } /** * 获取表数量 */ private function getTableCount($database) { try { $result = Db::query("SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = '{$database}'"); return intval($result[0]['count'] ?? 0); } catch (\Exception $e) { return 0; } } /** * 导出表到SQL文件 */ private function exportTableToSql($tableName, $filepath) { $fp = fopen($filepath, 'w'); // 写入表结构 $createTable = Db::query("SHOW CREATE TABLE `{$tableName}`"); fwrite($fp, "-- 表结构: {$tableName}\n"); fwrite($fp, "DROP TABLE IF EXISTS `{$tableName}`;\n"); fwrite($fp, $createTable[0]['Create Table'] . ";\n\n"); // 写入数据 $data = Db::query("SELECT * FROM `{$tableName}`"); if (!empty($data)) { fwrite($fp, "-- 表数据: {$tableName}\n"); foreach ($data as $row) { $values = []; foreach ($row as $value) { $values[] = is_null($value) ? 'NULL' : "'" . addslashes($value) . "'"; } fwrite($fp, "INSERT INTO `{$tableName}` VALUES (" . implode(', ', $values) . ");\n"); } } fclose($fp); } /** * 备份数据库(PHP方式) */ private function backupDatabase($database, $filepath) { $fp = fopen($filepath, 'w'); // 写入文件头 fwrite($fp, "-- MySQL数据库备份\n"); fwrite($fp, "-- 数据库: {$database}\n"); fwrite($fp, "-- 备份时间: " . date('Y-m-d H:i:s') . "\n"); fwrite($fp, "SET NAMES utf8mb4;\n"); fwrite($fp, "SET FOREIGN_KEY_CHECKS = 0;\n\n"); // 获取所有表 $tables = Db::query("SHOW TABLES FROM `{$database}`"); $tableKey = 'Tables_in_' . $database; foreach ($tables as $table) { $tableName = $table[$tableKey]; // 写入表结构 $createTable = Db::query("SHOW CREATE TABLE `{$tableName}`"); if (!empty($createTable)) { fwrite($fp, "-- ----------------------------\n"); fwrite($fp, "-- Table structure for {$tableName}\n"); fwrite($fp, "-- ----------------------------\n"); fwrite($fp, "DROP TABLE IF EXISTS `{$tableName}`;\n"); fwrite($fp, $createTable[0]['Create Table'] . ";\n\n"); // 写入数据 $data = Db::query("SELECT * FROM `{$tableName}`"); if (!empty($data)) { fwrite($fp, "-- ----------------------------\n"); fwrite($fp, "-- Records of {$tableName}\n"); fwrite($fp, "-- ----------------------------\n"); foreach ($data as $row) { $columns = []; $values = []; foreach ($row as $col => $val) { $columns[] = "`{$col}`"; $values[] = is_null($val) ? 'NULL' : "'" . addslashes($val) . "'"; } fwrite($fp, "INSERT INTO `{$tableName}` (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $values) . ");\n"); } fwrite($fp, "\n"); } } } fwrite($fp, "SET FOREIGN_KEY_CHECKS = 1;\n"); fclose($fp); } /** * 恢复数据库(PHP方式) */ private function restoreDatabase($filepath) { $sql = file_get_contents($filepath); // 分割SQL语句 $statements = array_filter(array_map('trim', explode(';', $sql))); foreach ($statements as $statement) { if (!empty($statement)) { Db::execute($statement); } } } /** * 查找mysqldump路径 */ private function findMysqldump() { $paths = [ '/usr/bin/mysqldump', '/usr/local/bin/mysqldump', 'C:\\mysql\\bin\\mysqldump.exe', 'C:\\xampp\\mysql\\bin\\mysqldump.exe', 'mysqldump' ]; foreach ($paths as $path) { if (is_executable($path) || shell_exec("which {$path}")) { return $path; } } return null; } /** * 查找mysql路径 */ private function findMysql() { $paths = [ '/usr/bin/mysql', '/usr/local/bin/mysql', 'C:\\mysql\\bin\\mysql.exe', 'C:\\xampp\\mysql\\bin\\mysql.exe', 'mysql' ]; foreach ($paths as $path) { if (is_executable($path) || shell_exec("which {$path}")) { return $path; } } return null; } /** * 上传备份文件到OSS * @param string $filepath 本地文件路径 * @param string $filename 文件名 * @return array|null 返回OSS URL和路径,失败返回null */ private function uploadBackupToOss($filepath, $filename) { if (!class_exists('\OSS\OssClient')) { throw new \RuntimeException('未安装 Aliyun OSS SDK,请先执行:composer require aliyuncs/oss-sdk-php'); } // 读取OSS配置 $uploadConfig = Config::get('upload.oss'); $accessKeyId = $uploadConfig['access_key_id'] ?? ''; $accessKeySecret = $uploadConfig['access_key_secret'] ?? ''; $endpoint = $uploadConfig['endpoint'] ?? ''; $bucket = $uploadConfig['bucket'] ?? ''; $baseUrl = rtrim($uploadConfig['url'] ?? '', '/'); // 如果配置为空,尝试从环境变量读取 if (empty($accessKeyId)) { $accessKeyId = getenv('OSS_ACCESS_KEY_ID') ?: getenv('ALIYUN_ACCESS_KEY_ID') ?: env('OSS_ACCESS_KEY_ID', env('ALIYUN_ACCESS_KEY_ID', '')); } if (empty($accessKeySecret)) { $accessKeySecret = getenv('OSS_ACCESS_KEY_SECRET') ?: getenv('ALIYUN_OSS_ACCESS_KEY_SECRET') ?: env('OSS_ACCESS_KEY_SECRET', env('ALIYUN_OSS_ACCESS_KEY_SECRET', '')); } if (empty($endpoint)) { $endpoint = getenv('OSS_ENDPOINT') ?: getenv('ALIYUN_OSS_ENDPOINT') ?: env('OSS_ENDPOINT', env('ALIYUN_OSS_ENDPOINT', '')); } if (empty($bucket)) { $bucket = getenv('OSS_BUCKET') ?: getenv('ALIYUN_OSS_BUCKET') ?: env('OSS_BUCKET', env('ALIYUN_OSS_BUCKET', '')); } if (empty($baseUrl)) { $baseUrl = rtrim(getenv('OSS_URL') ?: getenv('ALIYUN_OSS_URL') ?: env('OSS_URL', env('ALIYUN_OSS_URL', '')), '/'); } // 检查配置是否完整 if (empty($accessKeyId) || empty($accessKeySecret) || empty($endpoint) || empty($bucket) || empty($baseUrl)) { throw new \RuntimeException('OSS配置不完整,无法上传备份文件'); } // 构建OSS对象路径(不使用OSS_PREFIX,直接使用backup目录) // 格式:backup/2026/02/12/backup_mbti_20260212160100.sql $datePath = date('Y/m/d'); $object = 'backup/' . $datePath . '/' . $filename; try { // 创建OSS客户端 $client = new \OSS\OssClient($accessKeyId, $accessKeySecret, $endpoint); // 验证Bucket是否存在 if (!$client->doesBucketExist($bucket)) { throw new \RuntimeException("OSS Bucket '{$bucket}' 不存在或无法访问"); } // 上传文件 $client->uploadFile($bucket, $object, $filepath); // 生成访问URL $url = $baseUrl . '/' . ltrim($object, '/'); return [ 'url' => $url, 'path' => $object ]; } catch (\OSS\Core\OssException $e) { throw new \RuntimeException('OSS上传失败:' . $e->getMessage()); } } /** * 保存备份记录 */ private function saveBackupRecord($filename, $filepath, $fileSize, $ossUrl = null, $ossPath = null) { try { BackupRecordModel::create([ 'filename' => $filename, 'filepath' => $filepath, 'fileSize' => $fileSize, 'ossUrl' => $ossUrl, 'ossPath' => $ossPath, 'status' => 'success' ]); } catch (\Exception $e) { // 记录保存失败不影响备份成功,只记录日志 Log::error('保存备份记录失败:' . $e->getMessage()); } } }