Bài 20. Dự án: hệ thống quản lý sinh viên

Dự án tổng kết loạt PHP: xây dựng hệ thống quản lý sinh viên hoàn chỉnh bằng PHP thuần theo MVC, có đăng nhập, CRUD sinh viên và lớp, tìm kiếm, phân trang, upload ảnh, xuất CSV, API JSON và các biện pháp bảo mật cơ bản. Đây cũng là dạng đề thường gặp của bài tập lớn môn học.

180 phútNâng cao
Bạn sẽ học được
  • Phân tích yêu cầu, thiết kế CSDL và cấu trúc dự án
  • Áp dụng toàn bộ kiến thức: form, session, tệp, OOP, MVC, API
  • Kiểm tra bảo mật cơ bản trước khi nộp
  • Đóng gói, viết tài liệu và triển khai lên hosting

Khái niệm và lý thuyết

Yêu cầu chức năng: (1) Đăng nhập/đăng xuất, phân quyền admin và giảng viên; (2) Quản lý lớp: thêm, sửa, xóa; (3) Quản lý sinh viên: danh sách có tìm kiếm theo tên/mã, lọc theo lớp, phân trang 10 dòng; thêm, sửa, xóa, xem chi tiết; ảnh đại diện; (4) Nhập điểm theo môn, tính điểm trung bình và xếp loại; (5) Xuất danh sách CSV; (6) API JSON cho ứng dụng di động. Yêu cầu phi chức năng: chống SQL injection, XSS, CSRF; mật khẩu băm; thông báo lỗi thân thiện; giao diện responsive.

Thiết kế CSDL (qlsv)

CREATE TABLE lop (
  id INT AUTO_INCREMENT PRIMARY KEY,
  ma_lop VARCHAR(20) NOT NULL UNIQUE,
  ten_lop VARCHAR(100) NOT NULL,
  khoa VARCHAR(100)
);
CREATE TABLE sinhvien (
  id INT AUTO_INCREMENT PRIMARY KEY,
  ma_sv VARCHAR(10) NOT NULL UNIQUE,
  ho_ten VARCHAR(100) NOT NULL,
  ngay_sinh DATE,
  gioi_tinh ENUM('Nam','Nữ') DEFAULT 'Nam',
  email VARCHAR(100),
  anh VARCHAR(100),
  lop_id INT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (lop_id) REFERENCES lop(id)
);
CREATE TABLE diem (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sinhvien_id INT NOT NULL,
  mon VARCHAR(100) NOT NULL,
  diem DECIMAL(4,2) CHECK (diem BETWEEN 0 AND 10),
  FOREIGN KEY (sinhvien_id) REFERENCES sinhvien(id) ON DELETE CASCADE
);
CREATE TABLE nguoidung (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(100) NOT NULL UNIQUE,
  mat_khau_hash VARCHAR(255) NOT NULL,
  ho_ten VARCHAR(100),
  vai_tro ENUM('admin','giangvien') DEFAULT 'giangvien'
);

Cấu trúc dự án

qlsv/
├── public/index.php          # front controller, chỉ thư mục này công khai
├── public/css, js, uploads/
├── app/Core/{Database,Router,Request,View,Auth,Csrf}.php
├── app/Models/{SinhVien,Lop,Diem,NguoiDung}.php
├── app/Controllers/{AuthController,SinhVienController,LopController,DiemController,ApiController}.php
├── app/Views/{layout,auth/,sinhvien/,lop/,diem/}.php
├── config/{database,app}.php
├── database/qlsv.sql          # script tạo bảng + dữ liệu mẫu
└── README.md
RouteControllerGhi chú
GET/POST /login, GET /logoutAuthControllersession, password_verify
GET /sinhvien?q=&lop=&page=SinhVienController::indextìm kiếm, lọc, phân trang
GET/POST /sinhvien/them, /sinhvien/{id}/suaSinhVienControllervalidate, upload ảnh, CSRF
POST /sinhvien/{id}/xoaSinhVienController::xoachỉ admin
GET /sinhvien/xuat-csvSinhVienController::xuatCsvfputcsv, header tải xuống
GET/POST /diem/{sinhvien_id}DiemControllernhập điểm, điểm TB
GET /api/sinhvienApiControllerJSON, token

Những đoạn mã then chốt

<?php
// Phân trang + tìm kiếm trong Model
public static function search(string $q, int $lop_id, int $page, int $limit = 10): array {
    $db = Database::get(); $offset = ($page - 1) * $limit;
    $sql = "SELECT sv.*, l.ten_lop FROM sinhvien sv JOIN lop l ON l.id = sv.lop_id
            WHERE (sv.ho_ten LIKE ? OR sv.ma_sv LIKE ?) " . ($lop_id ? "AND sv.lop_id = ? " : "") .
           "ORDER BY sv.ho_ten LIMIT ? OFFSET ?";
    $st = $db->prepare($sql); $like = "%$q%";
    $lop_id ? $st->bind_param("ssiii", $like, $like, $lop_id, $limit, $offset)
            : $st->bind_param("ssii", $like, $like, $limit, $offset);
    $st->execute();
    return $st->get_result()->fetch_all(MYSQLI_ASSOC);
}
// Xuất CSV tải xuống
header("Content-Type: text/csv; charset=UTF-8");
header("Content-Disposition: attachment; filename=sinhvien.csv");
$out = fopen("php://output", "w"); fwrite($out, "\xEF\xBB\xBF");
fputcsv($out, ["Mã SV", "Họ tên", "Lớp", "Điểm TB"]);
foreach (SinhVien::all() as $sv) fputcsv($out, [$sv["ma_sv"], $sv["ho_ten"], $sv["ten_lop"], $sv["diem_tb"]]);
// Phân quyền trong Controller
if (Auth::user()["vai_tro"] !== "admin") { http_response_code(403); exit("Không có quyền"); }

Danh sách kiểm tra trước khi nộp

  • Mọi truy vấn dùng prepared statement; không nối chuỗi SQL.
  • Mọi đầu ra qua htmlspecialchars; form POST có CSRF token.
  • Mật khẩu băm bằng password_hash; session_regenerate_id khi đăng nhập.
  • Upload: kiểm tra MIME, đổi tên, chặn chạy PHP trong uploads.
  • display_errors tắt khi triển khai; lỗi ghi vào log.
  • CSDL có khóa ngoại, chỉ mục cho cột tìm kiếm (ma_sv, ho_ten).
  • README: cách cài, tài khoản mẫu, ảnh chụp màn hình.
  • Mã trên GitHub, không commit config chứa mật khẩu.

Triển khai

Hosting PHP/MySQL giá rẻ (hoặc gói miễn phí như InfinityFree) đủ cho đồ án: tải mã lên thư mục public_html bằng FTP/File Manager, tạo CSDL trong cPanel, nhập tệp qlsv.sql bằng phpMyAdmin, sửa config/database.php. Dùng tên miền phụ miễn phí hoặc tên miền .id.vn cho sinh viên.

Mẹo hay

Làm theo thứ tự: CSDL + dữ liệu mẫu → đăng nhập → CRUD sinh viên → tìm kiếm/phân trang → điểm → upload, CSV, API. Mỗi bước chạy được rồi mới sang bước sau và commit lên Git.

Lưu ý

Đừng dành quá nhiều thời gian cho giao diện trước khi chức năng chạy đúng. Bootstrap (loạt Framework web) giúp có giao diện gọn gàng nhanh chóng.

Thực hành
  1. Hoàn thiện dự án theo yêu cầu; nộp kèm qlsv.sql và README.
  2. Mở rộng: nhập sinh viên hàng loạt từ CSV, thống kê xếp loại theo lớp bằng biểu đồ (Chart.js).
  3. Viết ứng dụng Flutter hoặc trang JavaScript dùng API của dự án.
  4. Viết lại dự án bằng Laravel (bài 19) và so sánh số dòng mã.
Tóm tắt bài học
  • Dự án gồm CSDL 4 bảng, MVC, đăng nhập phân quyền, CRUD, tìm kiếm, phân trang, điểm, CSV, API.
  • Bảo mật là tiêu chí chấm: prepared statement, htmlspecialchars, CSRF, password_hash.
  • Làm từng bước, commit thường xuyên, viết README.
  • Triển khai lên hosting để có sản phẩm thật.