From fb44ec7afe19b52c0d1c92773acbca9ee351873d Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 15 Aug 2026 19:16:41 +0200 Subject: [PATCH] student_id-FK-Migration Stufe 1+2: Name-Joins -> student_sessions.student_id - student_sessions: neue Spalte student_id (+Index), backfilled aus (class_id,display_name) - Session::createStudent setzt student_id (per Lookup, falls nicht uebergeben) - alle Joins students<->student_sessions von display_name auf student_id umgestellt: Benchmark.php, live.php (5x), results.php (4x + runs), modules.php, profile.php, progress.php (answers+runs) - verlustfrei verifiziert (alt-Join == neu-Join, class 99/102) Robuster gegen Umbenennung/Doppelnamen, indexierbare Joins. Co-Authored-By: Claude Opus 4.8 --- App/php/api/live.php | 16 ++++++++-------- App/php/api/modules.php | 2 +- App/php/api/profile.php | 16 ++++++++++------ App/php/api/progress.php | 6 +++--- App/php/api/results.php | 12 ++++++------ App/php/lib/Benchmark.php | 3 +-- App/php/lib/Session.php | 17 +++++++++++++---- 7 files changed, 42 insertions(+), 30 deletions(-) diff --git a/App/php/api/live.php b/App/php/api/live.php index e31c405..26c7782 100644 --- a/App/php/api/live.php +++ b/App/php/api/live.php @@ -93,12 +93,12 @@ if ($method === 'GET') { s.id AS student_id, s.display_name, s.username, s.emoji_avatar, s.avatar_slug FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - JOIN students s ON s.class_id = ? AND s.display_name = ss.display_name + JOIN students s ON s.id = ss.student_id WHERE a.class_id = ? AND a.submitted_at >= ? AND a.submitted_at < ? ORDER BY s.id, a.sim_id, a.submitted_at ASC", - [$classId, $classId, $date . ' 00:00:00', $date . ' 23:59:59.999'] + [$classId, $date . ' 00:00:00', $date . ' 23:59:59.999'] ); // Helper: extrahiere "Lektion-Key" (mission/level/scenario) aus assessments-row @@ -394,10 +394,10 @@ if ($method === 'GET') { FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id WHERE a.class_id = ? AND a.sim_id = ? - AND ss.display_name = ? + AND ss.student_id = ? AND a.submitted_at BETWEEN ? AND ? ORDER BY a.submitted_at DESC', - [$classId, $simId, $student['display_name'], $start, $end] + [$classId, $simId, $studentId, $start, $end] ); if (!$rows) Response::ok(['results' => null, 'assessmentCount' => 0]); // Nimm den Row mit dem reichhaltigsten Results-JSON (Heuristik: hat .stars oder .results.totalStars) @@ -600,7 +600,7 @@ if ($method === 'GET') { $heuteRows = $db->fetchAll( "SELECT DISTINCT s.id AS student_id FROM students s - LEFT JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name + LEFT JOIN student_sessions ss ON ss.student_id = s.id LEFT JOIN assessments a ON a.session_id = ss.id AND a.submitted_at >= CURDATE() LEFT JOIN live_sessions ls ON ls.student_id = s.id AND ls.last_seen >= CURDATE() WHERE s.class_id = ? AND s.deleted_at IS NULL @@ -616,7 +616,7 @@ if ($method === 'GET') { MAX(GREATEST(IFNULL(a.submitted_at, '1970-01-01'), IFNULL(ls.last_seen, '1970-01-01'))) AS last_seen_any FROM students s - LEFT JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name + LEFT JOIN student_sessions ss ON ss.student_id = s.id LEFT JOIN assessments a ON a.session_id = ss.id LEFT JOIN live_sessions ls ON ls.student_id = s.id WHERE s.class_id = ? AND s.deleted_at IS NULL @@ -633,7 +633,7 @@ if ($method === 'GET') { "SELECT s.id AS student_id, a.sim_id, a.results, a.process_log, a.submitted_at FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name + JOIN students s ON s.id = ss.student_id WHERE a.class_id = ? AND s.deleted_at IS NULL ORDER BY s.id, a.sim_id, a.submitted_at DESC", [$classId] @@ -729,7 +729,7 @@ if ($method === 'GET') { SUM(COALESCE(a.duration_ms, 0)) / 1000 AS sec FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name + JOIN students s ON s.id = ss.student_id WHERE a.class_id = ? AND $sinceSql AND s.deleted_at IS NULL GROUP BY bucket, a.sim_id", [$classId] diff --git a/App/php/api/modules.php b/App/php/api/modules.php index fb908cb..c862733 100644 --- a/App/php/api/modules.php +++ b/App/php/api/modules.php @@ -74,7 +74,7 @@ if ($method === 'GET') { // Individuelle Overrides für diesen Schüler $studentRow = $db->fetchOne( - 'SELECT s.id FROM students s JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name WHERE ss.id = ?', + 'SELECT student_id AS id FROM student_sessions WHERE id = ?', [$sessionId] ); $overrides = []; diff --git a/App/php/api/profile.php b/App/php/api/profile.php index df21f02..9216118 100644 --- a/App/php/api/profile.php +++ b/App/php/api/profile.php @@ -16,21 +16,25 @@ if ($method === 'GET') { // ─── Sub-View: eigene Reflexionen einer Schüler:in ────────────────── if (($_GET['view'] ?? '') === 'reflections') { if (!$studentSessionId) Response::error('Nur eingeloggte Schüler:innen', 401); - $session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$studentSessionId]); + $session = $db->fetchOne('SELECT class_id, display_name, student_id FROM student_sessions WHERE id = ?', [$studentSessionId]); if (!$session) Response::error('Session nicht gefunden', 404); - // Reflexionen über alle assessments dieser Schüler:in (matched über - // class_id + display_name, weil verschiedene Sessions denselben - // Bearbeiter:innen-Namen tragen können). + // student_id (FK) statt fragilem display_name-Join; Fallback für seltene Alt-Sessions ohne FK. + $sid = $session['student_id'] ?? null; + if (!$sid) { + $st = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]); + $sid = $st ? (int)$st['id'] : 0; + } + // Reflexionen über alle Sessions dieser Schüler:in (per student_id verknüpft). $rows = $db->fetchAll( "SELECT a.sim_id, a.submitted_at, a.reflections FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - WHERE ss.class_id = ? AND ss.display_name = ? + WHERE ss.student_id = ? AND a.reflections IS NOT NULL AND JSON_LENGTH(a.reflections) > 0 ORDER BY a.submitted_at DESC LIMIT 50", - [$session['class_id'], $session['display_name']] + [$sid] ); $items = []; foreach ($rows as $r) { diff --git a/App/php/api/progress.php b/App/php/api/progress.php index d44b81e..6e6dfc1 100644 --- a/App/php/api/progress.php +++ b/App/php/api/progress.php @@ -44,7 +44,7 @@ if ($method === 'GET') { if ($studentId && $simId) { $progress = $db->fetchOne('SELECT level, xp, plays, best_stars FROM player_progress WHERE student_id = ? AND sim_id = ?', [$studentId, $simId]); - $answers = $db->fetchAll('SELECT phase, question_id, answer, correct, answered_at FROM assessment_answers WHERE session_id IN (SELECT id FROM student_sessions WHERE class_id = (SELECT class_id FROM students WHERE id = ?)) AND sim_id = ? ORDER BY answered_at DESC', [$studentId, $simId]); + $answers = $db->fetchAll('SELECT phase, question_id, answer, correct, answered_at FROM assessment_answers WHERE session_id IN (SELECT id FROM student_sessions WHERE student_id = ?) AND sim_id = ? ORDER BY answered_at DESC', [$studentId, $simId]); // Optional: alle Durchgänge mit results-JSON, wenn ?runs=1 → für Schüler-Eigen-Drilldown $runs = []; if (!empty($_GET['runs'])) { @@ -59,7 +59,7 @@ if ($method === 'GET') { "SELECT a.id, a.submitted_at, a.duration_ms, a.results FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - WHERE ss.class_id = ? AND ss.display_name = ? AND a.sim_id = ? + WHERE ss.student_id = ? AND a.sim_id = ? AND a.results IS NOT NULL AND ( JSON_EXTRACT(a.results, '$.stars') IS NOT NULL @@ -70,7 +70,7 @@ if ($method === 'GET') { ) ORDER BY a.submitted_at DESC LIMIT 50", - [$student['class_id'], $student['display_name'], $simId] + [$studentId, $simId] ); foreach ($rows as $row) { $row['results'] = $row['results'] ? json_decode($row['results'], true) : null; diff --git a/App/php/api/results.php b/App/php/api/results.php index a270b39..6b81987 100644 --- a/App/php/api/results.php +++ b/App/php/api/results.php @@ -206,7 +206,7 @@ if ($view === 'matrix') { "SELECT s.id AS student_id, a.sim_id, a.results, a.process_log FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name + JOIN students s ON s.id = ss.student_id WHERE a.class_id = ? AND s.deleted_at IS NULL ORDER BY s.id, a.sim_id, a.submitted_at DESC", [$classId] @@ -387,7 +387,7 @@ if ($view === 'fleiss') { COUNT(*) AS sessions FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name + JOIN students s ON s.id = ss.student_id WHERE a.class_id = ? AND $sinceSql AND s.deleted_at IS NULL $modSql GROUP BY s.id, bucket, a.sim_id", $params @@ -553,7 +553,7 @@ if ($view === 'activity') { "SELECT $bucketA AS bucket, s.id AS student_id, s.display_name AS name, s.emoji_avatar AS emoji, COUNT(*) AS n FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name + JOIN students s ON s.id = ss.student_id WHERE a.class_id = ? AND $sinceSqlA AND s.deleted_at IS NULL GROUP BY bucket, s.id", [$classId] @@ -656,10 +656,10 @@ if ($view === 'module') { "SELECT a.id, a.submitted_at, a.duration_ms, a.results, a.reflections FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - WHERE ss.class_id = ? AND ss.display_name = ? AND a.sim_id = ? + WHERE ss.student_id = ? AND a.sim_id = ? ORDER BY a.submitted_at DESC LIMIT 100", - [$classId, $student['display_name'], $moduleId] + [$studentId, $moduleId] ); // results + reflections JSON parsen (Frontend sieht's lieber dekodiert) foreach ($runs as &$run) { @@ -851,7 +851,7 @@ if ($view === 'reflections') { ss.display_name, s.id AS student_id, s.emoji_avatar FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - LEFT JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name AND s.deleted_at IS NULL + LEFT JOIN students s ON s.id = ss.student_id AND s.deleted_at IS NULL WHERE a.class_id = ? AND a.reflections IS NOT NULL AND JSON_LENGTH(a.reflections) > 0"; diff --git a/App/php/lib/Benchmark.php b/App/php/lib/Benchmark.php index be82c02..cbad53f 100644 --- a/App/php/lib/Benchmark.php +++ b/App/php/lib/Benchmark.php @@ -76,8 +76,7 @@ class Benchmark "SELECT a.sim_id, a.results, a.process_log, a.submitted_at FROM assessments a JOIN student_sessions ss ON ss.id = a.session_id - JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name - WHERE s.id = ? AND a.class_id = ? + WHERE ss.student_id = ? AND a.class_id = ? ORDER BY a.sim_id, a.submitted_at DESC", [$studentId, $classId] ); diff --git a/App/php/lib/Session.php b/App/php/lib/Session.php index 223c9c5..39c16f7 100644 --- a/App/php/lib/Session.php +++ b/App/php/lib/Session.php @@ -20,8 +20,10 @@ class Session { return $_COOKIE[self::COOKIE_NAME] ?? null; } - /** Neue Schueler-Session erstellen */ - public static function createStudent(int $classId, string $displayName = ''): string { + /** Neue Schueler-Session erstellen. student_id wird direkt gesetzt (per FK + * statt fragilem display_name-Join) — falls nicht übergeben, aus + * (class_id, display_name) aufgelöst. */ + public static function createStudent(int $classId, string $displayName = '', ?int $studentId = null): string { $uuid = sprintf('%s-%s-%s-%s-%s', bin2hex(random_bytes(4)), bin2hex(random_bytes(2)), @@ -31,9 +33,16 @@ class Session { ); $db = Database::get(); + if ($studentId === null && $displayName !== '') { + $row = $db->fetchOne( + 'SELECT id FROM students WHERE class_id = ? AND display_name = ? AND deleted_at IS NULL', + [$classId, $displayName] + ); + if ($row) $studentId = (int)$row['id']; + } $db->execute( - 'INSERT INTO student_sessions (id, class_id, display_name) VALUES (?, ?, ?)', - [$uuid, $classId, $displayName] + 'INSERT INTO student_sessions (id, class_id, display_name, student_id) VALUES (?, ?, ?, ?)', + [$uuid, $classId, $displayName, $studentId] ); setcookie(self::COOKIE_NAME, $uuid, [