chore: 首次提交 - 关联 GitHub fnvtk/MBTI_wang

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 12:39:38 +08:00
commit eb510304c0
259 changed files with 60464 additions and 0 deletions

View File

@@ -0,0 +1,750 @@
<?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());
}
}
}