323 lines
11 KiB
PHP
323 lines
11 KiB
PHP
<?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;
|
||
}
|
||
}
|