index() 表列表 * /admin/db/browse/ -> browse(table) 浏览(?q= 搜索、?page= 分页) * /admin/db/edit/
/ -> edit(table,id) 编辑表单 * /admin/db/update/
/ -> update(...) 保存(POST) * /admin/db/create/
-> create(table) 新增表单 * /admin/db/store/
-> store(...) 新增保存(POST) * /admin/db/delete/
/ -> delete(...) 删除(GET + 确认) * * 安全:表名与列名均来自 DESCRIBE / SHOW TABLES 白名单,杜绝 SQL 注入。 */ class DatabaseController extends Controller { protected $layout = 'layouts/admin'; /** 解析完整路径:['admin','db', action, table, id] */ private function route(): array { return \Core\App::parseRoute(); } /** 路由中的表名段(full[3]) */ private function segTable(): ?string { $r = $this->route(); return $r[3] ?? null; } /** 路由中的记录主键段(full[4]) */ private function segId(): ?string { $r = $this->route(); return $r[4] ?? null; } /** 表列表 */ public function index($ignored = null) { admin_required(); if (Db::driver() !== 'mysql') { return $this->view('admin/db_tables', [ 'fileMode' => true, 'dataFiles' => $this->fileDataFiles(), ]); } return $this->tablesList(); } /** 浏览记录 */ public function browse($table = null) { admin_required(); if (Db::driver() !== 'mysql') { return $this->tablesList(); } $table = $table ?? $this->segTable(); return $this->doBrowse($table); } /** 编辑 / 新增表单 */ public function edit($table = null) { admin_required(); if (Db::driver() !== 'mysql') { return $this->tablesList(); } $table = $table ?? $this->segTable(); $id = $this->segId(); return $this->doEditForm($table, $id); } /** 保存编辑 */ public function update($table = null) { admin_required(); $table = $table ?? $this->segTable(); $id = $this->segId(); return $this->doUpdate($table, $id); } /** 新增表单 */ public function create($table = null) { admin_required(); if (Db::driver() !== 'mysql') { return $this->tablesList(); } $table = $table ?? $this->segTable(); return $this->doEditForm($table, null); } /** 保存新增 */ public function store($table = null) { admin_required(); $table = $table ?? $this->segTable(); return $this->doStore($table); } /** 删除 */ public function delete($table = null) { admin_required(); $table = $table ?? $this->segTable(); $id = $this->segId(); return $this->doDelete($table, $id); } /* ---------------- 实现 ---------------- */ private function tablesList(): string { $pdo = Db::pdo(); $status = $pdo->query("SHOW TABLE STATUS")->fetchAll(); $tables = []; foreach ($status as $t) { $tables[] = [ 'name' => $t['Name'], 'engine' => $t['Engine'] ?? '', 'rows' => (int)($t['Rows'] ?? 0), 'size' => (int)($t['Data_length'] ?? 0) + (int)($t['Index_length'] ?? 0), 'collation' => $t['Collation'] ?? '', ]; } usort($tables, fn($a, $b) => strcmp($a['name'], $b['name'])); return $this->view('admin/db_tables', ['tables' => $tables, 'total' => count($tables)]); } private function doBrowse(?string $table): string { if (!$table || !$this->validTable($table)) { $this->flash('表不存在或无权访问', 'err'); return $this->tablesList(); } $pdo = Db::pdo(); $cols = $this->columnsOf($table); $names = array_column($cols, 'Field'); $pk = $this->pkOf($table) ?: $names[0]; $limit = 50; $page = max(1, (int)($_GET['page'] ?? 1)); $offset = ($page - 1) * $limit; $q = trim((string)($_GET['q'] ?? '')); $where = ''; $params = []; if ($q !== '') { $likes = []; foreach ($names as $c) { $likes[] = "`{$c}` LIKE ?"; $params[] = '%' . $q . '%'; } $where = ' WHERE ' . implode(' OR ', $likes); } $stmt = $pdo->prepare("SELECT COUNT(*) FROM `{$table}`" . $where); $stmt->execute($params); $total = (int)$stmt->fetchColumn(); $orderCol = in_array($pk, $names, true) ? $pk : $names[0]; $rows = $pdo->query("SELECT * FROM `{$table}`" . $where . " ORDER BY `{$orderCol}` DESC LIMIT {$limit} OFFSET {$offset}")->fetchAll(\PDO::FETCH_ASSOC); $totalPages = max(1, (int)ceil($total / $limit)); return $this->view('admin/db_browse', [ 'table' => $table, 'cols' => $cols, 'names' => $names, 'pk' => $pk, 'rows' => $rows, 'page' => $page, 'totalPages' => $totalPages, 'total' => $total, 'q' => $q, ]); } private function doEditForm(?string $table, $id): string { if (!$table || !$this->validTable($table)) { $this->flash('表不存在或无权访问', 'err'); return $this->tablesList(); } $pdo = Db::pdo(); $cols = $this->columnsOf($table); $pk = $this->pkOf($table); $row = null; if ($id !== null && $id !== '') { $stmt = $pdo->prepare("SELECT * FROM `{$table}` WHERE `{$pk}` = ? LIMIT 1"); $stmt->execute([$id]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!$row) { $this->flash('记录不存在', 'err'); return $this->doBrowse($table); } } return $this->view('admin/db_form', [ 'table' => $table, 'cols' => $cols, 'pk' => $pk, 'row' => $row, 'mode' => $row ? 'edit' : 'create', ]); } private function doUpdate(?string $table, $id): string { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect("admin/db/browse/$table"); return ''; } if (!$table || !$this->validTable($table)) { $this->flash('表不存在', 'err'); return $this->tablesList(); } $pdo = Db::pdo(); $cols = $this->columnsOf($table); $pk = $this->pkOf($table); $sets = []; $params = []; foreach ($cols as $c) { $f = $c['Field']; if ($f === $pk) continue; if (!array_key_exists($f, $_POST)) continue; $v = $_POST[$f]; if ($v === '' && $c['Null'] === 'YES') $v = null; $sets[] = "`{$f}` = ?"; $params[] = $v; } if (empty($sets)) { $this->flash('没有需要更新的字段', 'ok'); $this->redirect("admin/db/browse/$table"); return ''; } $params[] = $id; $pdo->prepare("UPDATE `{$table}` SET " . implode(', ', $sets) . " WHERE `{$pk}` = ?")->execute($params); $this->flash('记录已更新', 'ok'); $this->redirect("admin/db/browse/$table"); return ''; } private function doStore(?string $table): string { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect("admin/db/browse/$table"); return ''; } if (!$table || !$this->validTable($table)) { $this->flash('表不存在', 'err'); return $this->tablesList(); } $pdo = Db::pdo(); $cols = $this->columnsOf($table); $fields = []; $ph = []; $params = []; foreach ($cols as $c) { $f = $c['Field']; if (($c['Extra'] ?? '') === 'auto_increment') continue; if (!array_key_exists($f, $_POST)) { if ($c['Null'] === 'YES') { $fields[] = "`{$f}`"; $ph[] = '?'; $params[] = null; } continue; } $v = $_POST[$f]; if ($v === '' && $c['Null'] === 'YES') $v = null; $fields[] = "`{$f}`"; $ph[] = '?'; $params[] = $v; } if (empty($fields)) { $this->flash('没有可写入的字段', 'err'); $this->redirect("admin/db/browse/$table"); return ''; } $pdo->prepare("INSERT INTO `{$table}` (" . implode(', ', $fields) . ") VALUES (" . implode(', ', $ph) . ")") ->execute($params); $this->flash('记录已新增', 'ok'); $this->redirect("admin/db/browse/$table"); return ''; } private function doDelete(?string $table, $id): string { if (!$table || !$this->validTable($table)) { $this->flash('表不存在', 'err'); return $this->tablesList(); } $pk = $this->pkOf($table); Db::pdo()->prepare("DELETE FROM `{$table}` WHERE `{$pk}` = ?")->execute([$id]); $this->flash('记录已删除', 'ok'); $this->redirect("admin/db/browse/$table"); return ''; } /* ---------------- 工具方法 ---------------- */ /** 全部表名白名单(校验来自 URL 的表名) */ private function tableList(): array { return Db::pdo()->query("SHOW TABLES")->fetchAll(\PDO::FETCH_COLUMN); } private function validTable(string $t): bool { return in_array($t, $this->tableList(), true); } private function pkOf(string $t): ?string { $keys = Db::pdo()->query("SHOW KEYS FROM `{$t}` WHERE Key_name='PRIMARY'")->fetchAll(); if ($keys) return $keys[0]['Column_name']; $cols = $this->columnsOf($t); return $cols[0]['Field'] ?? null; } private function columnsOf(string $t): array { return Db::pdo()->query("DESCRIBE `{$t}`")->fetchAll(); } /** file 模式下可用的数据文件(只读提示) */ private function fileDataFiles(): array { $dir = Db::fileDir(); $out = []; foreach (glob($dir . '/*.json') as $f) { $out[] = ['name' => basename($f), 'size' => filesize($f)]; } return $out; } }