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;
}
}