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 <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
+10
-6
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
+13
-4
@@ -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, [
|
||||
|
||||
Reference in New Issue
Block a user