Files
MBTI_wang/api/app/controller/superadmin/Database.php
2026-03-17 11:53:41 +08:00

751 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\controller\superadmin;
use app\BaseController;
use app\model\BackupRecord as BackupRecordModel;
use think\facade\Request;
use think\facade\Db;
use think\facade\Config;
use think\facade\Log;
/**
* 数据库管理控制器(超管专用)
*/
class Database extends BaseController
{
/**
* 获取数据库信息
* @return \think\response\Json
*/
public function info()
{
// 验证是否为超级管理员
$user = $this->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());
}
}
}