Files
saqel/backend/app/Services/UnifiedExamService.php
T

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' => 'تحليل وتفكير عليا',
],
];
}
}