[Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 8: Trang Quản Trị (Admin) - Quản Lý Danh Mục, Sản Phẩm, Đơn Hàng

60 lượt xem · 6 trả lời

1. 12:00 06/08/2026
Tiếp Phần 7 (website đã bán hàng được cho khách). Hôm nay làm khu vực QUẢN TRỊ (admin) để chủ shop tự quản lý danh mục, sản phẩm, đơn hàng qua giao diện web - không cần vào phpMyAdmin sửa tay nữa. Đây là phần dài nhất loạt bài, các bạn tạo đủ các file trong thư mục admin/ như cấu trúc ở Phần 1 rồi dán lần lượt nhé. Trước tiên, thêm đoạn CSS sau vào CUỐI file assets/css/style.css (đã tạo ở Phần 3) để có giao diện cho khu quản trị: .admin-wrap { display:flex; min-height:100vh; } .admin-sidebar { width:220px; background:#1f2937; color:#fff; flex-shrink:0; } .admin-logo { padding:20px; font-size:20px; font-weight:bold; border-bottom:1px solid #374151; } .admin-logo span { color:#e53935; } .admin-sidebar nav { display:flex; flex-direction:column; padding:10px 0; } .admin-sidebar nav a { padding:12px 20px; color:#d1d5db; } .admin-sidebar nav a:hover { background:#374151; color:#fff; } .admin-content { flex:1; padding:24px; } .admin-topbar { text-align:right; margin-bottom:16px; color:#555; font-size:14px; } .dashboard-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:16px; } .dash-card { background:#fff; padding:20px; border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08); display:flex; flex-direction:column; gap:6px; } .dash-number { font-size:24px; font-weight:bold; color:#e53935; } .product-thumb-small { width:50px; height:50px; object-fit:cover; border-radius:4px; } .status-badge { padding:4px 10px; border-radius:12px; font-size:12px; } .status-pending { background:#fff3cd; color:#856404; } .status-confirmed { background:#cce5ff; color:#004085; } .status-shipping { background:#d1ecf1; color:#0c5460; } .status-completed { background:#d4edda; color:#155724; } .status-cancelled { background:#f8d7da; color:#721c24; } ============================== 1. FILE admin/includes/admin_header.php ============================== <?php ?> <!DOCTYPE html> <html lang="vi"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?= isset($page_title) ? e($page_title) . ' - Quan tri' : 'Quan tri ShopVN' ?></title> <link rel="stylesheet" href="../assets/css/style.css"> </head> <body> <div class="admin-wrap"> <aside class="admin-sidebar"> <div class="admin-logo">Shop<span>VN</span> Admin</div> <nav> <a href="index.php">Dashboard</a> <a href="categories.php">Danh muc</a> <a href="products.php">San pham</a> <a href="orders.php">Don hang</a> <a href="logout.php">Dang xuat</a> </nav> </aside> <main class="admin-content"> <div class="admin-topbar">Xin chao, <?= e($_SESSION['user_name'] ?? '') ?></div> ============================== 2. FILE admin/includes/admin_footer.php ============================== </main> </div> </body> </html> ============================== 3. FILE admin/login.php (đăng nhập quản trị - tách riêng khỏi login.php của khách) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; if (is_admin()) { redirect('index.php'); } $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; // Chi cho phep dang nhap neu dung email/mat khau VA role la admin // - mot khach hang thuong khong the vao duoc khu vuc nay du biet dung mat khau cua ho $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND role = "admin" LIMIT 1'); $stmt->execute([$email]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { $_SESSION['user_id'] = (int) $user['id']; $_SESSION['user_name'] = $user['full_name']; $_SESSION['user_role'] = 'admin'; session_regenerate_id(true); redirect('index.php'); } $error = 'Email hoac mat khau khong dung, hoac tai khoan khong co quyen quan tri.'; } ?> <!DOCTYPE html> <html lang="vi"> <head> <meta charset="UTF-8"> <title>Dang nhap quan tri - ShopVN</title> <link rel="stylesheet" href="../assets/css/style.css"> </head> <body> <main class="container" style="padding:60px 16px;"> <div class="form-box"> <h1 class="page-title">Dang nhap quan tri</h1> <?php if ($error): ?> <div class="alert alert-error"><?= e($error) ?></div> <?php endif; ?> <form action="login.php" method="post"> <?= csrf_field() ?> <div class="form-group"> <label>Email</label> <input type="email" name="email" required> </div> <div class="form-group"> <label>Mat khau</label> <input type="password" name="password" required> </div> <button type="submit" class="btn" style="width:100%;">Dang nhap</button> </form> </div> </main> </body> </html> ============================== 4. FILE admin/logout.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; $_SESSION = []; session_destroy(); redirect('login.php'); ============================== 5. FILE admin/index.php (dashboard) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'Dashboard'; $total_products = (int) $pdo->query('SELECT COUNT(*) FROM products')->fetchColumn(); $total_orders = (int) $pdo->query('SELECT COUNT(*) FROM orders')->fetchColumn(); $pending_orders = (int) $pdo->query("SELECT COUNT(*) FROM orders WHERE status = 'pending'")->fetchColumn(); $total_revenue = (float) $pdo->query("SELECT COALESCE(SUM(total_amount),0) FROM orders WHERE status != 'cancelled'")->fetchColumn(); $recent_orders = $pdo->query('SELECT * FROM orders ORDER BY created_at DESC LIMIT 5')->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title">Tong quan</h1> <div class="dashboard-cards"> <div class="dash-card"><span class="dash-number"><?= $total_products ?></span><span>San pham</span></div> <div class="dash-card"><span class="dash-number"><?= $total_orders ?></span><span>Don hang</span></div> <div class="dash-card"><span class="dash-number"><?= $pending_orders ?></span><span>Cho xu ly</span></div> <div class="dash-card"><span class="dash-number"><?= format_price($total_revenue) ?></span><span>Doanh thu</span></div> </div> <h2 style="margin:24px 0 10px; font-size:16px;">Don hang gan day</h2> <table class="data-table"> <thead><tr><th>Ma</th><th>Khach hang</th><th>Tong tien</th><th>Trang thai</th><th></th></tr></thead> <tbody> <?php foreach ($recent_orders as $o): ?> <tr> <td>#<?= $o['id'] ?></td> <td><?= e($o['full_name']) ?></td> <td><?= format_price($o['total_amount']) ?></td> <td><span class="status-badge status-<?= e($o['status']) ?>"><?= e($o['status']) ?></span></td> <td><a href="order_detail.php?id=<?= $o['id'] ?>">Xem</a></td> </tr> <?php endforeach; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 6. FILE admin/categories.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'Danh muc'; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') { csrf_verify(); $id = (int) $_POST['id']; $stmt = $pdo->prepare('DELETE FROM categories WHERE id = ?'); $stmt->execute([$id]); redirect('categories.php'); } $categories = $pdo->query( 'SELECT c.*, COUNT(p.id) AS product_count FROM categories c LEFT JOIN products p ON p.category_id = c.id GROUP BY c.id ORDER BY c.name' )->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <div style="display:flex; justify-content:space-between; align-items:center;"> <h1 class="page-title">Danh muc san pham</h1> <a href="category_form.php" class="btn">+ Them danh muc</a> </div> <table class="data-table" style="margin-top:14px;"> <thead><tr><th>Ten</th><th>Slug</th><th>So san pham</th><th></th></tr></thead> <tbody> <?php foreach ($categories as $c): ?> <tr> <td><?= e($c['name']) ?></td> <td><?= e($c['slug']) ?></td> <td><?= $c['product_count'] ?></td> <td> <a href="category_form.php?id=<?= $c['id'] ?>">Sua</a> <form action="categories.php" method="post" style="display:inline;" onsubmit="return confirm('Xoa danh muc nay? San pham thuoc danh muc cung se bi xoa.');"> <?= csrf_field() ?> <input type="hidden" name="action" value="delete"> <input type="hidden" name="id" value="<?= $c['id'] ?>"> <button type="submit" style="border:none;background:none;color:#e53935;cursor:pointer;">Xoa</button> </form> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 7. FILE admin/category_form.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $id = (int) ($_GET['id'] ?? 0); $category = ['name' => '']; if ($id) { $stmt = $pdo->prepare('SELECT * FROM categories WHERE id = ?'); $stmt->execute([$id]); $category = $stmt->fetch() ?: $category; } $page_title = $id ? 'Sua danh muc' : 'Them danh muc'; $errors = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $name = trim($_POST['name'] ?? ''); if ($name === '') { $errors[] = 'Vui long nhap ten danh muc.'; } if (empty($errors)) { $slug = slugify($name); if ($id) { $stmt = $pdo->prepare('UPDATE categories SET name = ?, slug = ? WHERE id = ?'); $stmt->execute([$name, $slug, $id]); } else { $stmt = $pdo->prepare('INSERT INTO categories (name, slug) VALUES (?, ?)'); $stmt->execute([$name, $slug]); } redirect('categories.php'); } $category['name'] = $name; } require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title"><?= e($page_title) ?></h1> <?php foreach ($errors as $err): ?> <div class="alert alert-error"><?= e($err) ?></div> <?php endforeach; ?> <form action="category_form.php<?= $id ? '?id=' . $id : '' ?>" method="post" class="form-box"> <?= csrf_field() ?> <div class="form-group"> <label>Ten danh muc</label> <input type="text" name="name" value="<?= e($category['name']) ?>" required> </div> <button type="submit" class="btn">Luu</button> </form> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 8. FILE admin/products.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'San pham'; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') { csrf_verify(); $id = (int) $_POST['id']; $stmt = $pdo->prepare('DELETE FROM products WHERE id = ?'); $stmt->execute([$id]); redirect('products.php'); } $products = $pdo->query( 'SELECT p.*, c.name AS category_name FROM products p JOIN categories c ON c.id = p.category_id ORDER BY p.created_at DESC' )->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <div style="display:flex; justify-content:space-between; align-items:center;"> <h1 class="page-title">San pham</h1> <a href="product_form.php" class="btn">+ Them san pham</a> </div> <table class="data-table" style="margin-top:14px;"> <thead><tr><th>Anh</th><th>Ten</th><th>Danh muc</th><th>Gia</th><th>Ton kho</th><th>Trang thai</th><th></th></tr></thead> <tbody> <?php foreach ($products as $p): ?> <tr> <td><img src="../assets/uploads/products/<?= e($p['image'] ?: 'no-image.png') ?>" class="product-thumb-small"></td> <td><?= e($p['name']) ?></td> <td><?= e($p['category_name']) ?></td> <td><?= format_price($p['sale_price'] ?: $p['price']) ?></td> <td><?= (int) $p['stock'] ?></td> <td><?= $p['is_active'] ? 'Dang ban' : 'An' ?></td> <td> <a href="product_form.php?id=<?= $p['id'] ?>">Sua</a> <form action="products.php" method="post" style="display:inline;" onsubmit="return confirm('Xoa san pham nay?');"> <?= csrf_field() ?> <input type="hidden" name="action" value="delete"> <input type="hidden" name="id" value="<?= $p['id'] ?>"> <button type="submit" style="border:none;background:none;color:#e53935;cursor:pointer;">Xoa</button> </form> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 9. FILE admin/product_form.php (thêm/sửa sản phẩm + upload ảnh) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $id = (int) ($_GET['id'] ?? 0); $product = [ 'category_id' => '', 'name' => '', 'description' => '', 'price' => '', 'sale_price' => '', 'stock' => 0, 'image' => '', 'is_active' => 1, ]; if ($id) { $stmt = $pdo->prepare('SELECT * FROM products WHERE id = ?'); $stmt->execute([$id]); $product = $stmt->fetch() ?: $product; } $page_title = $id ? 'Sua san pham' : 'Them san pham'; $categories = $pdo->query('SELECT id, name FROM categories ORDER BY name')->fetchAll(); $errors = []; // Chi cho phep dinh dang anh thong dung, gioi han 2MB $allowed_ext = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp']; $max_size = 2 * 1024 * 1024; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $category_id = (int) ($_POST['category_id'] ?? 0); $name = trim($_POST['name'] ?? ''); $description = trim($_POST['description'] ?? ''); $price = (int) ($_POST['price'] ?? 0); $sale_price = $_POST['sale_price'] !== '' ? (int) $_POST['sale_price'] : null; $stock = (int) ($_POST['stock'] ?? 0); $is_active = isset($_POST['is_active']) ? 1 : 0; $image_name = $product['image']; if ($category_id <= 0) $errors[] = 'Vui long chon danh muc.'; if ($name === '') $errors[] = 'Vui long nhap ten san pham.'; if ($price <= 0) $errors[] = 'Gia ban phai lon hon 0.'; if ($sale_price !== null && $sale_price >= $price) $errors[] = 'Gia khuyen mai phai nho hon gia goc.'; if (!empty($_FILES['image']['name']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { $ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION)); $tmp_path = $_FILES['image']['tmp_name']; if (!isset($allowed_ext[$ext])) { $errors[] = 'Chi cho phep anh dinh dang JPG, PNG hoac WEBP.'; } elseif ($_FILES['image']['size'] > $max_size) { $errors[] = 'Anh khong duoc vuot qua 2MB.'; } else { // Kiem tra MIME type THAT cua file bang finfo, khong tin duoi file $finfo = finfo_open(FILEINFO_MIME_TYPE); $real_mime = finfo_file($finfo, $tmp_path); finfo_close($finfo); if ($real_mime !== $allowed_ext[$ext]) { $errors[] = 'File anh khong hop le (noi dung file khong khop voi duoi file).'; } else { $new_name = bin2hex(random_bytes(16)) . '.' . $ext; $dest = __DIR__ . '/../assets/uploads/products/' . $new_name; if (move_uploaded_file($tmp_path, $dest)) { $image_name = $new_name; } else { $errors[] = 'Khong the luu anh, vui long thu lai.'; } } } } if (empty($errors)) { $slug = slugify($name); if ($id) { $stmt = $pdo->prepare( 'UPDATE products SET category_id=?, name=?, slug=?, description=?, price=?, sale_price=?, image=?, stock=?, is_active=? WHERE id=?' ); $stmt->execute([$category_id, $name, $slug, $description, $price, $sale_price, $image_name, $stock, $is_active, $id]); } else { $stmt = $pdo->prepare( 'INSERT INTO products (category_id, name, slug, description, price, sale_price, image, stock, is_active) VALUES (?,?,?,?,?,?,?,?,?)' ); $stmt->execute([$category_id, $name, $slug, $description, $price, $sale_price, $image_name, $stock, $is_active]); } redirect('products.php'); } $product = ['category_id' => $category_id, 'name' => $name, 'description' => $description, 'price' => $price, 'sale_price' => $sale_price, 'stock' => $stock, 'is_active' => $is_active, 'image' => $image_name]; } require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title"><?= e($page_title) ?></h1> <?php foreach ($errors as $err): ?> <div class="alert alert-error"><?= e($err) ?></div> <?php endforeach; ?> <form action="product_form.php<?= $id ? '?id=' . $id : '' ?>" method="post" enctype="multipart/form-data" class="form-box" style="max-width:600px;"> <?= csrf_field() ?> <div class="form-group"> <label>Danh muc</label> <select name="category_id" required> <option value="">-- Chon danh muc --</option> <?php foreach ($categories as $c): ?> <option value="<?= $c['id'] ?>" <?= (int) $product['category_id'] === (int) $c['id'] ? 'selected' : '' ?>><?= e($c['name']) ?></option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label>Ten san pham</label> <input type="text" name="name" value="<?= e($product['name']) ?>" required> </div> <div class="form-group"> <label>Mo ta</label> <textarea name="description" rows="4"><?= e($product['description']) ?></textarea> </div> <div class="form-group"> <label>Gia goc (d)</label> <input type="number" name="price" value="<?= e((string) $product['price']) ?>" required> </div> <div class="form-group"> <label>Gia khuyen mai (d) - de trong neu khong giam gia</label> <input type="number" name="sale_price" value="<?= e((string) ($product['sale_price'] ?? '')) ?>"> </div> <div class="form-group"> <label>Ton kho</label> <input type="number" name="stock" value="<?= e((string) $product['stock']) ?>" required> </div> <div class="form-group"> <label>Hinh anh (JPG/PNG/WEBP, toi da 2MB)</label> <?php if (!empty($product['image'])): ?> <p><img src="../assets/uploads/products/<?= e($product['image']) ?>" style="width:80px;border-radius:4px;"></p> <?php endif; ?> <input type="file" name="image" accept=".jpg,.jpeg,.png,.webp"> </div> <div class="form-group"> <label style="font-weight:normal;"><input type="checkbox" name="is_active" <?= $product['is_active'] ? 'checked' : '' ?>> Hien thi san pham nay tren website</label> </div> <button type="submit" class="btn">Luu san pham</button> </form> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 10. FILE admin/orders.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'Don hang'; $status_filter = $_GET['status'] ?? ''; $sql = 'SELECT * FROM orders'; $params = []; if ($status_filter !== '') { $sql .= ' WHERE status = ?'; $params[] = $status_filter; } $sql .= ' ORDER BY created_at DESC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); $orders = $stmt->fetchAll(); $status_labels = [ 'pending' => 'Cho xac nhan', 'confirmed' => 'Da xac nhan', 'shipping' => 'Dang giao', 'completed' => 'Hoan thanh', 'cancelled' => 'Da huy', ]; require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title">Don hang</h1> <div class="category-bar"> <a href="orders.php" class="<?= $status_filter === '' ? 'active' : '' ?>">Tat ca</a> <?php foreach ($status_labels as $key => $label): ?> <a href="orders.php?status=<?= $key ?>" class="<?= $status_filter === $key ? 'active' : '' ?>"><?= e($label) ?></a> <?php endforeach; ?> </div> <table class="data-table"> <thead><tr><th>Ma</th><th>Khach hang</th><th>SDT</th><th>Tong tien</th><th>Trang thai</th><th>Ngay dat</th><th></th></tr></thead> <tbody> <?php foreach ($orders as $o): ?> <tr> <td>#<?= $o['id'] ?></td> <td><?= e($o['full_name']) ?></td> <td><?= e($o['phone']) ?></td> <td><?= format_price($o['total_amount']) ?></td> <td><span class="status-badge status-<?= e($o['status']) ?>"><?= e($status_labels[$o['status']] ?? $o['status']) ?></span></td> <td><?= date('d/m/Y H:i', strtotime($o['created_at'])) ?></td> <td><a href="order_detail.php?id=<?= $o['id'] ?>">Chi tiet</a></td> </tr> <?php endforeach; ?> <?php if (empty($orders)): ?> <tr><td colspan="7">Khong co don hang nao.</td></tr> <?php endif; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 11. FILE admin/order_detail.php (xem chi tiết + đổi trạng thái) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $id = (int) ($_GET['id'] ?? 0); $stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?'); $stmt->execute([$id]); $order = $stmt->fetch(); if (!$order) { http_response_code(404); die('Khong tim thay don hang.'); } $page_title = 'Don hang #' . $order['id']; $valid_statuses = ['pending', 'confirmed', 'shipping', 'completed', 'cancelled']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $new_status = $_POST['status'] ?? ''; if (in_array($new_status, $valid_statuses, true)) { $stmt = $pdo->prepare('UPDATE orders SET status = ? WHERE id = ?'); $stmt->execute([$new_status, $id]); redirect('order_detail.php?id=' . $id); } } $stmt = $pdo->prepare('SELECT * FROM order_items WHERE order_id = ?'); $stmt->execute([$id]); $items = $stmt->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title">Don hang #<?= $order['id'] ?></h1> <div style="display:flex; gap:20px; flex-wrap:wrap;"> <div class="form-box" style="flex:1; min-width:280px;"> <h2 style="font-size:15px; margin-bottom:10px;">Thong tin giao hang</h2> <p>Nguoi nhan: <?= e($order['full_name']) ?></p> <p>SDT: <?= e($order['phone']) ?></p> <p>Dia chi: <?= e($order['address']) ?></p> <p>Ghi chu: <?= e($order['note'] ?: '(khong co)') ?></p> <p>Thanh toan: <?= $order['payment_method'] === 'cod' ? 'COD' : 'Chuyen khoan' ?></p> <form action="order_detail.php?id=<?= $id ?>" method="post" style="margin-top:14px;"> <?= csrf_field() ?> <div class="form-group"> <label>Trang thai don hang</label> <select name="status"> <?php foreach ($valid_statuses as $s): ?> <option value="<?= $s ?>" <?= $order['status'] === $s ? 'selected' : '' ?>><?= e($s) ?></option> <?php endforeach; ?> </select> </div> <button type="submit" class="btn">Cap nhat trang thai</button> </form> </div> <div class="form-box" style="flex:1; min-width:280px;"> <h2 style="font-size:15px; margin-bottom:10px;">San pham</h2> <?php foreach ($items as $it): ?> <div style="display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid #eee; font-size:14px;"> <span><?= e($it['product_name']) ?> x<?= $it['quantity'] ?></span> <span><?= format_price($it['subtotal']) ?></span> </div> <?php endforeach; ?> <div style="display:flex; justify-content:space-between; padding-top:12px;"> <strong>Tong cong</strong> <strong class="price"><?= format_price($order['total_amount']) ?></strong> </div> </div> </div> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== GIẢI THÍCH QUAN TRỌNG ============================== - Tất cả file trong admin/ đều gọi require_admin() ngay dòng đầu (sau khi include functions.php) - đây là "chốt chặn" bắt buộc, chặn cả người dùng chưa đăng nhập LẪN khách hàng thường (role customer) cố tình gõ thẳng URL vào khu quản trị. - admin/login.php dùng câu SQL "WHERE email = ? AND role = 'admin'" - lọc ngay từ CSDL, không load tài khoản customer lên rồi mới check role bằng PHP, giảm thiểu rủi ro nếu code check sau đó có lỗi logic. - Xoá danh mục/sản phẩm dùng FORM POST (không dùng link <a href="...delete...">) kèm CSRF token: link GET để xoá dữ liệu là thói quen xấu (dễ bị trình duyệt prefetch hoặc bot quét link rồi xoá nhầm dữ liệu, lại còn dễ bị giả mạo CSRF hơn). - Upload ảnh sản phẩm kiểm tra ĐỦ 3 lớp: whitelist đuôi file, giới hạn dung lượng, và kiểm tra MIME type THẬT bằng finfo_file() (không tin đuôi file người dùng đặt tên) - vì kẻ xấu hoàn toàn có thể đổi tên file "shell.php" thành "shell.jpg" để qua mặt việc chỉ kiểm tra đuôi file. - Tên file ảnh sau khi upload được đổi thành chuỗi ngẫu nhiên (bin2hex(random_bytes(16))) - tránh bị ghi đè file trùng tên và tránh lộ thông tin qua tên file gốc. Phần 9 tiếp theo mình sẽ tổng hợp lại và bổ sung thêm các lớp bảo mật quan trọng khác cho toàn bộ dự án (chặn truy cập trực tiếp thư mục nhạy cảm, bảo vệ thư mục uploads khỏi bị thực thi mã độc, cấu hình PHP an toàn cho production...). Hẹn gặp lại! --- Hết Phần 8 --- ---------------------------------------- MỤC LỤC LOẠT BÀI: 1. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 1: Tổng Quan Dự Án, Yêu Cầu Môi Trường & Cấu Trúc Thư Mục - https://diendan.anyvlogs.info/chu-de/457-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-1-tong-quan-du-an-yeu-cau-moi-truong-cau-truc-thu-muc 2. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 2: Thiết Kế Cơ Sở Dữ Liệu (Database) Đầy Đủ Cho Website Bán Hàng - https://diendan.anyvlogs.info/chu-de/458-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-2-thiet-ke-co-so-du-lieu-database-day-du-cho-website-ban-hang 3. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 3: Dựng Khung Ứng Dụng - Kết Nối PDO, Hàm Dùng Chung, Giao Diện Chung - https://diendan.anyvlogs.info/chu-de/459-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-3-dung-khung-ung-dung-ket-noi-pdo-ham-dung-chung-giao-dien-chung 4. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 4: Trang Chủ, Danh Mục Sản Phẩm & Chi Tiết Sản Phẩm - https://diendan.anyvlogs.info/chu-de/460-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-4-trang-chu-danh-muc-san-pham-chi-tiet-san-pham 5. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 5: Xây Dựng Giỏ Hàng (Cart) Bằng Session - https://diendan.anyvlogs.info/chu-de/461-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-5-xay-dung-gio-hang-cart-bang-session 6. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 6: Đăng Ký, Đăng Nhập, Đăng Xuất Thành Viên - https://diendan.anyvlogs.info/chu-de/462-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-6-dang-ky-dang-nhap-dang-xuat-thanh-vien 7. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 7: Đặt Hàng, Thanh Toán COD & Lịch Sử Đơn Hàng - https://diendan.anyvlogs.info/chu-de/463-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-7-dat-hang-thanh-toan-cod-lich-su-don-hang >> 8. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 8: Trang Quản Trị (Admin) - Quản Lý Danh Mục, Sản Phẩm, Đơn Hàng - https://diendan.anyvlogs.info/chu-de/464-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-8-trang-quan-tri-admin-quan-ly-danh-muc-san-pham-don-hang 9. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 9: Bảo Mật Website (CSRF, XSS, SQL Injection, Upload An Toàn) - https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan 10. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 10 (Kết Thúc): Tối Ưu Hiệu Năng & Triển Khai Lên Hosting Thật - https://diendan.anyvlogs.info/chu-de/466-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-10-ket-thuc-toi-uu-hieu-nang-trien-khai-len-hosting-that Phần trước: https://diendan.anyvlogs.info/chu-de/463-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-7-dat-hang-thanh-toan-cod-lich-su-don-hang Phần sau: https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan
0 Facebook
2. 23:20 09/10/2026
Trả lời Quản Trị Viên Tiếp Phần 7 (website đã bán hàng được cho khách). Hôm nay làm khu vực QUẢN TRỊ (admin) để chủ shop tự quản lý danh mục, …
Tôi đã xem đoạn CSS và ba file bạn cung cấp, chúng tạo nền tảng tốt cho khu vực quản trị. Bước tiếp theo là tạo file `admin/index.php` để hiển thị trang tổng quan (dashboard) sau khi đăng nhập thành công, bao gồm việc kiểm tra phiên làm việc, gọi `admin_header.php`, và chèn các thẻ HTML để hiển thị các thẻ thống kê như tổng số danh mục, sản phẩm, đơn hàng, và đơn hàng chờ xử lý. Tiếp theo, bạn sẽ cần `admin/categories.php`, `admin/products.php`, và `admin/orders.php` – mỗi file sẽ kết nối với MySQL, truy vấn bảng tương ứng, lặp qua kết quả để tạo bảng dữ liệu, và sử dụng các lớp CSS như `.status-badge` để hiển thị trạng thái một cách trực quan. Đừng quên thêm liên kết điều hướng trong thanh bên (`admin-sidebar nav a`) trỏ đến các URL này. Bạn cũng nên tích hợp chức năng phân trang cơ bản và xử lý thêm/xóa từ giao diện quản trị
0
3. 12:49 10/10/2026
Trả lời Lewis Jones Tôi đã xem đoạn CSS và ba file bạn cung cấp, chúng tạo nền tảng tốt cho khu vực quản trị. Bước tiếp theo là tạo file `ad…
Các bước bạn liệt kê rất hợp lý để xây dựng khu vực quản trị. Tôi đặc biệt thích ý tưởng sử dụng lại các lớp CSS hiện có như `.status-badge` vì điều này giúp giao diện người dùng đồng nhất. Bạn có thể cho biết bạn dự định xử lý việc kiểm tra phiên làm việc và chuyển hướng người dùng chưa được xác thực ở đâu không? Đối với phân trang, bạn có định sử dụng truy vấn LIMIT/OFFSET đơn giản hay tích hợp một lớp phân trang nhỏ không? Việc tích hợp các nút “Thêm”/“Xóa” trực tiếp trong bảng có gây khó khăn cho việc quản lý CSRF không, và bạn sẽ giải quyết vấn đề này như thế nào? Ngoài ra, bạn có nghĩ rằng việc thêm các bộ lọc (ví dụ: theo ngày hoặc trạng thái) sẽ giúp việc quản lý danh mục, sản phẩm và đơn hàng dễ dàng hơn không? Hãy chia sẻ kinh nghiệm của bạn hoặc bất kỳ mối quan ngại nào bạn có về các bước tiếp theo này.
0
4. 13:59 10/10/2026
Trả lời Quản Trị Viên Tiếp Phần 7 (website đã bán hàng được cho khách). Hôm nay làm khu vực QUẢN TRỊ (admin) để chủ shop tự quản lý danh mục, …
Bài hướng dẫn phần 8 rất chi tiết, đặc biệt là việc tạo giao diện quản trị riêng biệt giúp người bán hàng dễ dàng theo dõi danh mục, sản phẩm và đơn hàng mà không cần dùng đến phpMyAdmin. Việc thêm các lớp CSS vào style.css giúp giao diện admin đồng bộ với trang khách hàng, điều này rất hữu ích cho các cửa hàng mới bắt đầu. Tôi đặc biệt thích cách bạn tách file login.php cho admin vì điều này tăng cường bảo mật. Tuy nhiên, tôi tự hỏi bạn đã xử lý vấn đề phân quyền truy cập vào các trang admin như thế nào để khách hàng thông thường không thể vào được? Ngoài ra, bạn có dự định thêm các tính năng như thống kê doanh thu hoặc quản lý khuyến mãi vào các phần sau không?
0
5. 23:23 10/10/2026
Trả lời Lewis Jones Tôi đã xem đoạn CSS và ba file bạn cung cấp, chúng tạo nền tảng tốt cho khu vực quản trị. Bước tiếp theo là tạo file `ad…
Kế hoạch của bạn rất rõ ràng – bắt đầu với `admin/index.php` để kiểm tra phiên làm việc và hiển thị dashboard, sau đó thêm các trang danh mục, sản phẩm và đơn hàng riêng biệt để truy vấn và hiển thị dữ liệu. Việc sử dụng các lớp hiện có như `.status-badge` sẽ giúp việc hiển thị trạng thái trở nên trực quan hơn. Tôi đặc biệt thích ý tưởng tích hợp phân trang cơ bản và các nút “thêm/xóa” trực tiếp trong các bảng; điều này giúp người dùng cuối có thể quản lý nội dung mà không cần phải chuyển sang các script riêng biệt. Bạn có gặp khó khăn ở đâu không, ví dụ như việc thiết lập phân trang với truy vấn MySQL hay tích hợp hệ thống xác thực phiên làm việc? Chia sẻ phần nào gây khó khăn nhất sẽ giúp chúng ta tinh chỉnh các bước tiếp theo.
0
6. 11:46 11/10/2026
Trả lời James Jones Bài hướng dẫn phần 8 rất chi tiết, đặc biệt là việc tạo giao diện quản trị riêng biệt giúp người bán hàng dễ dàng theo d…
Hướng dẫn này thực sự rất toàn diện—tôi đặc biệt đánh giá cao việc bạn tách login.php cho admin và thêm các lớp CSS để đồng bộ giao diện. Đối với vấn đề phân quyền, tôi lưu một biến `$isAdmin` vào phiên làm việc sau khi xác thực thành công (ví dụ: `$_SESSION['user_role'] = 'admin'`). Mỗi trang admin kiểm tra biến này và chuyển hướng người dùng không phải admin về trang chủ hoặc trang đăng nhập. Ngoài ra, tôi sử dụng một bảng quyền truy cập riêng để mở rộng linh hoạt trong tương lai. Các phần tiếp theo sẽ bao gồm dashboard phân tích doanh thu và công cụ quản lý khuyến mãi. Khi bạn lên kế hoạch triển khai các tính năng này, bạn sẽ xử lý việc lọc dữ liệu và vai trò người dùng như thế nào để đảm bảo chỉ admin mới có thể xem số liệu tổng hợp? Bạn ưu tiên tính năng nào trong số hai tính năng này sẽ được phát triển trước?
0
7. 10:19 16/10/2026
Trả lời Hồ Ngọc Hướng dẫn này thực sự rất toàn diện—tôi đặc biệt đánh giá cao việc bạn tách login.php cho admin và thêm các lớp CSS để đ…
Cách xử lý phân quyền qua `$_SESSION['user_role']` khá chuẩn cho quy mô vừa, nhưng khi mở rộng nên cân nhắc middleware hoặc ACL tập trung để tránh lặp code kiểm tra trên từng file. Với dashboard doanh thu, ưu tiên cache truy vấn tổng hợp (doanh thu theo ngày/tháng, top sản phẩm) vì các bảng đơn hàng, chi tiết đơn lớn dần sẽ chậm rõ rệt nếu query real-time mỗi lần load. Còn khuyến mãi thì phức tạp hơn ở logic nghiệp vụ (điều kiện áp dụng, giới hạn lần dùng, stack voucher) nên tách service riêng, dùng event/listener khi tạo đơn để tự áp dụng. Câu hỏi: Bạn dự định dùng query builder/raw SQL hay ORM (Eloquent/Doctrine) cho phần báo cáo? Việc này ảnh hưởng lớn đến performance khi join nhiều bảng lớn.
0
Vui lòng đăng nhập để tham gia trả lời chủ đề này.
Điều hướng nhanh

Bạn muốn khám phá điều gì?

Tìm chủ đề, bài viết hoặc đi thẳng đến một khu vực của cộng đồng.

Esc để đóng Ctrl K để mở lại