Files
coolcoth.com/app/Controllers/Admin/DatabaseController.php
T
2026-08-08 15:53:53 +08:00

323 lines
11 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\Controllers\Admin;
use App\Controllers\Controller;
use Core\App;
use Core\Db;
/**
* 后台:数据库管理
* - 列出全部数据表(MySQL 模式)。
* - 浏览任意表数据、查看/编辑/新增/删除记录(基于主键)。
* - 仅 MySQL 模式可用;file 模式仅做说明提示。
*
* 路由说明:App::dispatchAdmin 会以 URL 段作为方法名调用本控制器,
* 故 browse/edit/update/create/store/delete 均为 public,并在内部用
* App::parseRoute() 重新解析完整路径(含第 3、4 段)以拿到表名与记录主键。
* /admin/db -> index() 表列表
* /admin/db/browse/<table> -> browse(table) 浏览(?q= 搜索、?page= 分页)
* /admin/db/edit/<table>/<id> -> edit(table,id) 编辑表单
* /admin/db/update/<table>/<id> -> update(...) 保存(POST
* /admin/db/create/<table> -> create(table) 新增表单
* /admin/db/store/<table> -> store(...) 新增保存(POST
* /admin/db/delete/<table>/<id> -> 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;
}
}