文件还在测试中
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user