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
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);
?>
= h($crawlMsg) ?>
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.
4. Tạo đề thi từ câu hỏi đã xuất bản (= count($published) ?> 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);
?>
Giải thích: = h($res['question']['explanation']) ?>
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);
?>