* @LastEditTime: 2026-09-28 00:00:00 * @Description: 数据库备份 / 还原 / 优化 * @FilePath: \ywxapp_dev\app\backend\controller\Database.php */ declare(strict_types=1); namespace app\backend\controller; use think\facade\Db; use ywxapp\controller\BackendBase; use ywxapp\library\DbBackup; /** * 数据库管理 * * 权限节点(首次访问自动补录到 backend_power,挂到「系统管理」下): * database:index / database:backup / database:recovery / database:delete / database:optimize * * @author ywxapp */ class Database extends BackendBase { use EnsuresPower; // 权限节点自愈:首次访问自动补录 backend_power protected $noNeedLogin = []; protected $noNeedVerify = []; /** * @var DbBackup */ protected $bak; /** * 进程内只补录一次权限 * @var bool */ protected static $powerChecked = false; protected function initialize() { $this->bak = new DbBackup(); if (!self::$powerChecked) { self::$powerChecked = true; $this->ensureMenus(); } } /** * 列表:数据表 + 备份记录 */ public function index() { if ($this->request->isAjax()) { $type = $this->request->param('type', 'table'); if ($type === 'backup') { $list = $this->bak->backups(); $this->result->setCount(count($list))->success(array_map(function ($item) { $item['size_text'] = $this->formatBytes($item['size']); $item['time_text'] = date('Y-m-d H:i:s', $item['time']); return $item; }, $list)); } $page = $this->request->param('page/d', 1); $limit = $this->request->param('limit/d', 15); $kw = trim((string) $this->request->param('keyword', '')); $tables = $this->bak->tables(); if ($kw !== '') { $tables = array_values(array_filter($tables, function ($t) use ($kw) { return stripos($t['name'], $kw) !== false || stripos($t['comment'], $kw) !== false; })); } $total = count($tables); $slice = array_slice($tables, ($page - 1) * $limit, $limit); foreach ($slice as &$t) { $t['size_text'] = $this->formatBytes($t['size']); } $this->result->setCount($total)->success($slice); } return $this->fetch(); } /** * 备份 */ public function backup() { if (! $this->request->isPost()) { $this->result->error('请求方式错误', 405); } $tables = $this->request->param('tables/a', []); $size = $this->request->param('part_size/d', 2097152); $size = max(65536, min($size, 52428800)); // 64KB ~ 50MB set_time_limit(0); try { $res = $this->bak->backup(array_values(array_filter((array) $tables)), null); } catch (\Throwable $e) { \think\facade\Log::error('数据库备份失败:' . $e->getMessage()); $this->result->error('备份失败:' . $e->getMessage()); } $res['size_text'] = $this->formatBytes($res['size']); $this->result->success($res, '备份完成:' . $res['tables'] . ' 张表 / ' . $res['rows'] . ' 行'); } /** * 还原 * * 注意:基类 BackendBase 已有 restore($ids='')(回收站恢复),此处不能用同名, * 否则签名不兼容会直接 500,故命名为 recovery。 */ public function recovery() { if (! $this->request->isPost()) { $this->result->error('请求方式错误', 405); } $name = (string) $this->request->param('name', ''); if ($name === '') { $this->result->error('请选择备份'); } set_time_limit(0); try { $res = $this->bak->restore($name); } catch (\Throwable $e) { \think\facade\Log::error('数据库还原失败:' . $e->getMessage()); $this->result->error('还原失败:' . $e->getMessage()); } $this->result->success($res, '还原完成:' . $res['files'] . ' 个文件 / ' . $res['sql'] . ' 条语句'); } /** * 删除备份 */ public function delete() { if (! $this->request->isPost()) { $this->result->error('请求方式错误', 405); } $name = (string) $this->request->param('name', ''); if ($name === '') { $this->result->error('请选择备份'); } $this->result->success(['ok' => $this->bak->delete($name)], '已删除'); } /** * 下载备份文件 */ public function download() { $name = (string) $this->request->param('name', ''); $file = (string) $this->request->param('file', ''); $path = $this->bak->fileOf($name, $file); if (! $path || ! is_file($path)) { $this->result->error('备份文件不存在', 404); } return response(file_get_contents($path), 200, [ 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="' . basename($path) . '"', 'Content-Length' => filesize($path), ]); } /** * 优化表 */ public function optimize() { if (! $this->request->isPost()) { $this->result->error('请求方式错误', 405); } $tables = array_values(array_filter((array) $this->request->param('tables/a', []))); if (! $tables) { $this->result->error('请选择要优化的表'); } $res = $this->bak->optimize($tables); $this->result->success($res, '已优化 ' . count($res) . ' 张表'); } /** * 权限节点补录:核心后台菜单即权限记录,缺失时自动补上, * 避免手工在「权限菜单」里一条条加。 */ protected function formatBytes(int $bytes): string { if ($bytes <= 0) { return '0 B'; } $units = ['B', 'KB', 'MB', 'GB']; $i = (int) min(count($units) - 1, floor(log($bytes, 1024))); return round($bytes / (1024 ** $i), 2) . ' ' . $units[$i]; } }