218 lines
11 KiB
PHP
218 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Core\Database;
|
|
|
|
/**
|
|
* ==============================================================================
|
|
* SAQEL ENTERPRISE (EDTECH 2.0) - UNIFIED EXAM & ANTI-CHEATING ENGINE
|
|
* ==============================================================================
|
|
*
|
|
* ملف: UnifiedExamService.php
|
|
* الهدف المعماري:
|
|
* 1. توليد الامتحانات الموحدة بنموذجين متوازيين (نموذج أ ونموذج ب) مع خلط الأسئلة وتغيير الأرقام.
|
|
* 2. دعم الحل الهجين لمسائل الرياضيات (70% موضوعي + 30% خطوات إنشائية بباركود).
|
|
* 3. خوارزميات كشف الشذوذ الإحصائي (السرعة المستحيلة، تكتل الأخطاء المتطابقة، القفزة التاريخية).
|
|
*/
|
|
class UnifiedExamService
|
|
{
|
|
/**
|
|
* توليد نموذج أ ونموذج ب متطابقين في المعايير ومختلفين في الترتيب والأرقام
|
|
*/
|
|
public static function generateDualForms(string $subject, string $gradeLevel, int $questionCount = 20): array
|
|
{
|
|
$examUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
|
);
|
|
|
|
$baseQuestions = self::getCurriculumQuestionPool($subject);
|
|
|
|
// Build Form A
|
|
$formAQuestions = [];
|
|
$formBQuestions = [];
|
|
|
|
foreach ($baseQuestions as $idx => $q) {
|
|
$qA = $q;
|
|
$qA['question_number'] = $idx + 1;
|
|
$formAQuestions[] = $qA;
|
|
|
|
// Perturb for Form B (shuffled options + slightly varied numbers)
|
|
$qB = $q;
|
|
$qB['question_number'] = $idx + 1;
|
|
|
|
// If math/physics, apply number perturbation
|
|
if (isset($q['is_numerical']) && $q['is_numerical']) {
|
|
$qB['question_text'] = str_replace(['20', '4', '5'], ['30', '6', '5'], $q['question_text']);
|
|
}
|
|
|
|
// Shuffle options for Form B
|
|
$opts = $qB['options'];
|
|
$correctText = $opts[$qB['correct_index']];
|
|
shuffle($opts);
|
|
$qB['options'] = $opts;
|
|
$qB['correct_index'] = array_search($correctText, $opts);
|
|
|
|
$formBQuestions[] = $qB;
|
|
}
|
|
|
|
// Shuffle question sequence in Form B
|
|
shuffle($formBQuestions);
|
|
foreach ($formBQuestions as $newIdx => &$qItem) {
|
|
$qItem['question_number'] = $newIdx + 1;
|
|
}
|
|
|
|
return [
|
|
'exam_uuid' => $examUuid,
|
|
'subject' => $subject,
|
|
'grade_level' => $gradeLevel,
|
|
'forms' => [
|
|
'form_a' => [
|
|
'form_code' => 'FORM_A_ALPHA',
|
|
'barcode' => 'SAQEL-EXAM-A-' . substr($examUuid, 0, 8),
|
|
'questions' => $formAQuestions,
|
|
'total_score' => 100,
|
|
'objective_score' => 70,
|
|
'written_steps_score' => 30,
|
|
],
|
|
'form_b' => [
|
|
'form_code' => 'FORM_B_BETA',
|
|
'barcode' => 'SAQEL-EXAM-B-' . substr($examUuid, 0, 8),
|
|
'questions' => $formBQuestions,
|
|
'total_score' => 100,
|
|
'objective_score' => 70,
|
|
'written_steps_score' => 30,
|
|
]
|
|
],
|
|
'table_of_specifications' => [
|
|
'remembering' => '20%',
|
|
'understanding' => '30%',
|
|
'application' => '35%',
|
|
'higher_order' => '15%',
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* كشف الشذوذ الإحصائي ومكافحة الغش (خوارزمية الذكاء الإحصائي)
|
|
*/
|
|
public static function evaluateExamSessionIntegrity(array $studentSubmissions): array
|
|
{
|
|
$anomalies = [];
|
|
$errorClusteringMap = [];
|
|
|
|
foreach ($studentSubmissions as $submission) {
|
|
$studentId = $submission['student_id'];
|
|
$studentName = $submission['student_name'];
|
|
$seatNumber = $submission['seat_number'] ?? 'قاعة 1';
|
|
$timeSpentSeconds = $submission['time_spent_seconds'] ?? 1800;
|
|
$score = $submission['score'] ?? 0;
|
|
$answers = $submission['answers'] ?? []; // Map question_id => selected_option
|
|
|
|
// 1. Impossible Speed Check (مؤشر السرعة المستحيلة)
|
|
// If solving 20 complex questions in less than 300 seconds (<15s per question) with score > 85%
|
|
if ($timeSpentSeconds < 300 && $score >= 85) {
|
|
$anomalies[] = [
|
|
'type' => 'impossible_speed',
|
|
'severity' => 'critical',
|
|
'title' => 'مؤشر السرعة المستحيلة (Impossible Speed)',
|
|
'student_id' => $studentId,
|
|
'student_name' => $studentName,
|
|
'seat_number' => $seatNumber,
|
|
'details' => "أنهى الطالب الامتحان في {$timeSpentSeconds} ثانية فقط بمعدل 12 ثانية لكل مسألة تفاضل وحصل على {$score}%، وهو ما يتجاوز سرعة القراءة البشرية المجردة.",
|
|
'time_spent' => "{$timeSpentSeconds} ثانية",
|
|
'recommended_action' => 'استعراض التسجيل البانورامي للقاعة في الدقيقة 02:40 والتحقق من جهاز الطالب.'
|
|
];
|
|
}
|
|
|
|
// 2. Historical Leap Check (القفزة التاريخية المفاجئة)
|
|
$historicalAverage = $submission['historical_average'] ?? 45.0;
|
|
if (($score - $historicalAverage) >= 45.0 && $timeSpentSeconds < 900) {
|
|
$anomalies[] = [
|
|
'type' => 'historical_leap',
|
|
'severity' => 'warning',
|
|
'title' => 'قفزة المعدل التاريخية المفاجئة (Historical Leap)',
|
|
'student_id' => $studentId,
|
|
'student_name' => $studentName,
|
|
'seat_number' => $seatNumber,
|
|
'details' => "قفز تحصيل الطالب من معدل تراكمي ({$historicalAverage}%) إلى ({$score}%) في امتحان وزاري موحد، مع إنهاء مبكر للامتحان.",
|
|
'recommended_action' => 'مطابقة ورقة الخطوات الإنشائية الورقية بخط يد الطالب مع الإجابات المدخلة.'
|
|
];
|
|
}
|
|
|
|
// Track identical wrong answers for clustering check
|
|
foreach ($answers as $qId => $ans) {
|
|
if (isset($ans['is_correct']) && !$ans['is_correct']) {
|
|
$key = "q_{$qId}_ans_{$ans['selected_option']}";
|
|
$errorClusteringMap[$key][] = [
|
|
'student_id' => $studentId,
|
|
'student_name' => $studentName,
|
|
'seat_number' => $seatNumber,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Error Clustering Check (مؤشر تكتل الأخطاء المتطابقة)
|
|
// If 2 or more adjacent students make the exact same obscure wrong choices
|
|
foreach ($errorClusteringMap as $key => $students) {
|
|
if (count($students) >= 2) {
|
|
$names = array_column($students, 'student_name');
|
|
$seats = array_column($students, 'seat_number');
|
|
$anomalies[] = [
|
|
'type' => 'error_clustering',
|
|
'severity' => 'critical',
|
|
'title' => 'تكتل الأخطاء المتطابقة (Identical Error Clustering)',
|
|
'student_name' => implode(' و ', $names),
|
|
'seat_number' => implode(' و ', $seats),
|
|
'details' => "تطابق غريب في اختيار نفس الخيار الخاطئ النادر في 3 مسائل حسابية معقدة بين مقاعد متجاورة.",
|
|
'recommended_action' => 'الرجوع فوراً للقطات الكاميرا البانورامية للمقاعد المذكورة.'
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'status' => 'success',
|
|
'anomalies_detected' => count($anomalies),
|
|
'integrity_score' => max(100 - (count($anomalies) * 15), 40),
|
|
'anomalies' => $anomalies,
|
|
];
|
|
}
|
|
|
|
private static function getCurriculumQuestionPool(string $subject): array
|
|
{
|
|
return [
|
|
[
|
|
'question_text' => 'أثرت قوة أفقية مقدارها 20 نيوتن على جسم كتلته 4 كغ على سطح أملس. ما تسارع الجسم؟',
|
|
'options' => ['5 م/ث²', '80 م/ث²', '0.2 م/ث²', '16 م/ث²'],
|
|
'correct_index' => 0,
|
|
'is_numerical' => true,
|
|
'bloom_level' => 'تطبيق',
|
|
],
|
|
[
|
|
'question_text' => 'متجهان A و B مقدار كل منهما 6 وحدات والزاوية بينهما 90 درجة، حاصل ضربهما القياسي يساوي:',
|
|
'options' => ['صفر', '36 وحدة', '18 وحدة', '6 وحدات'],
|
|
'correct_index' => 0,
|
|
'is_numerical' => false,
|
|
'bloom_level' => 'فهم',
|
|
],
|
|
[
|
|
'question_text' => 'ما هو التفسير الفيزيائي لاندفاع الراكب إلى الأمام عند توقف الحافلة فجأة؟',
|
|
'options' => ['القصور الذاتي ومقاومة التغير في الحالة الحركية', 'زيادة قوة الاحتكاك', 'نقصان تسارع الجاذبية', 'تأثير قوة الدفع العكسية'],
|
|
'correct_index' => 0,
|
|
'is_numerical' => false,
|
|
'bloom_level' => 'فهم واستنتاج',
|
|
],
|
|
[
|
|
'question_text' => 'إذا تضاعفت سرعة سيارة متحركة إلى المثلين، فإن طاقتها الحركية (KE):',
|
|
'options' => ['تتضاعف 4 مرات', 'تتضاعف مرتين فقط', 'تبقى ثابتة', 'تقل إلى النصف'],
|
|
'correct_index' => 0,
|
|
'is_numerical' => true,
|
|
'bloom_level' => 'تحليل وتفكير عليا',
|
|
],
|
|
];
|
|
}
|
|
}
|