PDO::ERRMODE_EXCEPTION] ); } catch (Exception $e) { die("Không kết nối được database. Kiểm tra lại DB_HOST/DB_NAME/DB_USER/DB_PASS trong file. Lỗi: " . $e->getMessage()); } // ==================== TỰ ĐỘNG TẠO BẢNG (chạy 1 lần, sau đó bỏ qua) ==================== $pdo->exec("CREATE TABLE IF NOT EXISTS subjects ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, slug VARCHAR(100) NOT NULL UNIQUE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS topics ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, subject_id INT UNSIGNED NOT NULL, name VARCHAR(150) NOT NULL, FOREIGN KEY (subject_id) REFERENCES subjects(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS sources ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(200) NOT NULL, type ENUM('official_exam','crawled','ai_generated','manual') NOT NULL DEFAULT 'manual', url VARCHAR(500) NULL, auto_recrawl BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS questions ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, subject_id INT UNSIGNED NOT NULL, topic_id INT UNSIGNED NULL, source_id INT UNSIGNED NULL, content TEXT NOT NULL, difficulty ENUM('nhan_biet','thong_hieu','van_dung','van_dung_cao') NOT NULL DEFAULT 'thong_hieu', explanation TEXT NULL, status ENUM('draft','published') NOT NULL DEFAULT 'draft', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (subject_id) REFERENCES subjects(id) ON DELETE CASCADE, FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE SET NULL, FOREIGN KEY (source_id) REFERENCES sources(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS answer_choices ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, question_id INT UNSIGNED NOT NULL, label VARCHAR(5) NOT NULL, content TEXT NOT NULL, is_correct BOOLEAN NOT NULL DEFAULT FALSE, FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS exams ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, subject_id INT UNSIGNED NOT NULL, title VARCHAR(255) NOT NULL, is_published BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (subject_id) REFERENCES subjects(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS exam_questions ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, exam_id INT UNSIGNED NOT NULL, question_id INT UNSIGNED NOT NULL, order_no SMALLINT UNSIGNED NOT NULL DEFAULT 1, FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE, FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE, UNIQUE KEY uniq_eq (exam_id, question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); // Seed môn Toán nếu bảng subjects đang trống $count = $pdo->query("SELECT COUNT(*) FROM subjects")->fetchColumn(); if ($count == 0) { $pdo->exec("INSERT INTO subjects (name, slug) VALUES ('Toán', 'toan')"); $sid = $pdo->lastInsertId(); foreach (['Hàm số','Mũ và Logarit','Nguyên hàm - Tích phân','Số phức','Hình học không gian','Hình học tọa độ Oxyz','Tổ hợp - Xác suất'] as $t) { $pdo->prepare("INSERT INTO topics (subject_id, name) VALUES (?, ?)")->execute([$sid, $t]); } } // ==================== HÀM TIỆN ÍCH ==================== function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function isAdmin() { return !empty($_SESSION['is_admin']); } function redirect($r) { header("Location: ?r=" . $r); exit; } // Bóc tách câu hỏi từ HTML thô theo mẫu phổ biến "Câu 1:" ... "A." "B." "C." "D." // Đây là bóc tách TỔNG QUÁT (heuristic) - áp dụng tốt với các trang trình bày // đề thi dạng văn bản thuần. Với trang có cấu trúc đặc biệt, bạn nên chỉnh // lại hàm này hoặc dùng chức năng "Import theo định dạng chuẩn" (chính xác hơn). function genericParseExam(string $html): array { $text = strip_tags(str_ireplace(['
', '
', '
', '

', ''], "\n", $html)); $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); $text = preg_replace("/\n{2,}/", "\n", $text); $blocks = preg_split('/(?=Câu\s*\d+[:.\)])/u', $text); $questions = []; foreach ($blocks as $block) { $block = trim($block); if (!preg_match('/^Câu\s*\d+[:.\)]/u', $block)) continue; if (!preg_match_all('/^([A-D])[.\)]\s*(.+)$/mu', $block, $m, PREG_SET_ORDER)) continue; if (count($m) < 2) continue; $firstChoicePos = mb_strpos($block, $m[0][0]); $content = trim(mb_substr($block, 0, $firstChoicePos)); $content = preg_replace('/^Câu\s*\d+[:.\)]\s*/u', '', $content); if (mb_strlen($content) < 3) continue; $choices = []; $correctLabel = null; if (preg_match('/Đáp\s*án\s*[:\-]?\s*([A-D])/iu', $block, $am)) { $correctLabel = strtoupper($am[1]); } foreach ($m as $choiceMatch) { $label = strtoupper($choiceMatch[1]); $choices[] = [ 'label' => $label, 'content' => trim($choiceMatch[2]), 'is_correct' => ($correctLabel !== null && $label === $correctLabel), ]; } $questions[] = ['content' => $content, 'choices' => $choices]; } return $questions; } function saveQuestions(PDO $pdo, int $subjectId, int $sourceId, array $questions): int { $saved = 0; foreach ($questions as $q) { $check = $pdo->prepare("SELECT id FROM questions WHERE subject_id=? AND content=? LIMIT 1"); $check->execute([$subjectId, $q['content']]); if ($check->fetch()) continue; $stmt = $pdo->prepare("INSERT INTO questions (subject_id, source_id, content, status) VALUES (?, ?, ?, 'draft')"); $stmt->execute([$subjectId, $sourceId, $q['content']]); $qid = $pdo->lastInsertId(); $cs = $pdo->prepare("INSERT INTO answer_choices (question_id, label, content, is_correct) VALUES (?, ?, ?, ?)"); foreach ($q['choices'] as $c) { $cs->execute([$qid, $c['label'], $c['content'], $c['is_correct'] ? 1 : 0]); } $saved++; } return $saved; } // ==================== ROUTER ==================== $r = $_GET['r'] ?? 'home'; // --- Cron endpoint (gọi từ cPanel Cron Job qua wget/curl, không cần đăng nhập, bảo vệ bằng key) --- if ($r === 'cron_crawl') { if (($_GET['key'] ?? '') !== CRON_SECRET_KEY) { http_response_code(403); die('Sai key'); } $sources = $pdo->query("SELECT * FROM sources WHERE auto_recrawl = 1 AND url IS NOT NULL")->fetchAll(PDO::FETCH_ASSOC); $total = 0; foreach ($sources as $src) { $html = @file_get_contents($src['url']); if (!$html) continue; $qs = genericParseExam($html); $subjectRow = $pdo->query("SELECT id FROM subjects LIMIT 1")->fetch(); $total += saveQuestions($pdo, (int)$subjectRow['id'], (int)$src['id'], $qs); } die("Cron OK - đã lưu $total câu hỏi mới."); } // --- Đăng nhập / đăng xuất admin --- if ($r === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (($_POST['password'] ?? '') === ADMIN_PASSWORD) { $_SESSION['is_admin'] = true; redirect('admin'); } $loginError = 'Sai mật khẩu'; } if ($r === 'logout') { unset($_SESSION['is_admin']); redirect('home'); } // --- Xử lý form admin (POST) --- if ($r === 'admin_crawl' && isAdmin() && $_SERVER['REQUEST_METHOD'] === 'POST') { $url = trim($_POST['url']); $subjectId = (int)$_POST['subject_id']; $autoRecrawl = isset($_POST['auto_recrawl']) ? 1 : 0; $html = @file_get_contents($url); if ($html === false) { $crawlMsg = "Không tải được trang. Kiểm tra lại URL hoặc trang có chặn truy cập tự động."; } else { $srcStmt = $pdo->prepare("INSERT INTO sources (name, type, url, auto_recrawl) VALUES (?, 'crawled', ?, ?)"); $srcStmt->execute(["Crawl: $url", $url, $autoRecrawl]); $sourceId = $pdo->lastInsertId(); $qs = genericParseExam($html); $saved = saveQuestions($pdo, $subjectId, $sourceId, $qs); $crawlMsg = "Quét xong: tìm thấy " . count($qs) . " câu, lưu mới $saved câu (trạng thái draft, cần duyệt)."; } } if ($r === 'admin_import_text' && isAdmin() && $_SERVER['REQUEST_METHOD'] === 'POST') { $subjectId = (int)$_POST['subject_id']; $srcStmt = $pdo->prepare("INSERT INTO sources (name, type) VALUES ('Nhập tay theo định dạng chuẩn', 'manual')"); $srcStmt->execute(); $sourceId = $pdo->lastInsertId(); $qs = genericParseExam($_POST['raw_text']); $saved = saveQuestions($pdo, $subjectId, $sourceId, $qs); $crawlMsg = "Import xong: tìm thấy " . count($qs) . " câu, lưu mới $saved câu."; } if ($r === 'admin_publish' && isAdmin()) { $pdo->prepare("UPDATE questions SET status='published' WHERE id=?")->execute([(int)$_GET['id']]); redirect('admin'); } if ($r === 'admin_delete_q' && isAdmin()) { $pdo->prepare("DELETE FROM questions WHERE id=?")->execute([(int)$_GET['id']]); redirect('admin'); } if ($r === 'admin_create_exam' && isAdmin() && $_SERVER['REQUEST_METHOD'] === 'POST') { $subjectId = (int)$_POST['subject_id']; $title = trim($_POST['title']); $qIds = $_POST['question_ids'] ?? []; $pdo->prepare("INSERT INTO exams (subject_id, title, is_published) VALUES (?, ?, 1)")->execute([$subjectId, $title]); $examId = $pdo->lastInsertId(); $order = 1; $insQ = $pdo->prepare("INSERT INTO exam_questions (exam_id, question_id, order_no) VALUES (?, ?, ?)"); foreach ($qIds as $qid) { $insQ->execute([$examId, (int)$qid, $order++]); } redirect('admin'); } // --- Nộp bài thi (học sinh) --- if ($r === 'submit_exam' && $_SERVER['REQUEST_METHOD'] === 'POST') { $examId = (int)$_POST['exam_id']; $answers = $_POST['answers'] ?? []; // question_id => choice_id $questions = $pdo->prepare(" SELECT q.id, q.content, q.explanation FROM exam_questions eq JOIN questions q ON q.id = eq.question_id WHERE eq.exam_id = ? ORDER BY eq.order_no"); $questions->execute([$examId]); $questions = $questions->fetchAll(PDO::FETCH_ASSOC); $correctCount = 0; $results = []; foreach ($questions as $q) { $choices = $pdo->prepare("SELECT * FROM answer_choices WHERE question_id=?"); $choices->execute([$q['id']]); $choices = $choices->fetchAll(PDO::FETCH_ASSOC); $chosenId = $answers[$q['id']] ?? null; $correctChoice = null; foreach ($choices as $c) { if ($c['is_correct']) $correctChoice = $c; } $isCorrect = $chosenId && $correctChoice && (int)$chosenId === (int)$correctChoice['id']; if ($isCorrect) $correctCount++; $results[] = ['question' => $q, 'choices' => $choices, 'chosen_id' => $chosenId, 'is_correct' => $isCorrect, 'correct_choice' => $correctChoice]; } $total = count($questions); $score = $total > 0 ? round(($correctCount / $total) * 10, 2) : 0; } // ==================== GIAO DIỆN (HTML) ==================== function layout(string $title, string $body): void { global $r; echo ""; echo ""; echo "" . h($title) . ""; echo "
Ôn Thi THPT QG
Trang chủ"; echo isAdmin() ? "Quản trịĐăng xuất" : "Quản trị"; echo "
" . $body . "
"; } ob_start(); if ($r === 'login') { ?>

Đăng nhập quản trị

query("SELECT * FROM subjects")->fetchAll(PDO::FETCH_ASSOC); $drafts = $pdo->query("SELECT * FROM questions WHERE status='draft' ORDER BY id DESC LIMIT 50")->fetchAll(PDO::FETCH_ASSOC); $published = $pdo->query("SELECT * FROM questions WHERE status='published' ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC); ?>

1. Quét đề từ URL website

Lưu ý: chỉ hoạt động tốt với trang trình bày đề dạng "Câu 1: ... A. ... B. ... C. ... D. ...". Trang có cấu trúc phức tạp (JS render, ảnh) có thể không quét được — khi đó dùng cách nhập tay bên dưới.

2. Import theo định dạng chuẩn (dán văn bản)

3. Câu hỏi chờ duyệt (draft) —

Duyệt & Xuất bản Xóa

Không có câu nào đang chờ.

4. Tạo đề thi từ câu hỏi đã xuất bản ( câu)

prepare("SELECT * FROM exams WHERE id=?"); $exam->execute([$examId]); $exam = $exam->fetch(PDO::FETCH_ASSOC); $qs = $pdo->prepare("SELECT q.* FROM exam_questions eq JOIN questions q ON q.id=eq.question_id WHERE eq.exam_id=? ORDER BY eq.order_no"); $qs->execute([$examId]); $qs = $qs->fetchAll(PDO::FETCH_ASSOC); ?>

$q): $choices = $pdo->prepare("SELECT * FROM answer_choices WHERE question_id=?"); $choices->execute([$q['id']]); $choices = $choices->fetchAll(PDO::FETCH_ASSOC); ?>

Câu :

Kết quả: /10 (/ câu đúng)

$res): ?>

Câu :

.

Giải thích:

query("SELECT * FROM subjects")->fetchAll(PDO::FETCH_ASSOC); $exams = $pdo->query("SELECT e.*, s.name as subject_name FROM exams e JOIN subjects s ON s.id=e.subject_id WHERE e.is_published=1 ORDER BY e.id DESC")->fetchAll(PDO::FETCH_ASSOC); ?>

Danh sách đề thi

Chưa có đề thi nào được xuất bản. Vào trang Quản trị để tạo đề.