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
|
s.id AS student_id, s.display_name, s.username, s.emoji_avatar, s.avatar_slug
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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 = ?
|
WHERE a.class_id = ?
|
||||||
AND a.submitted_at >= ?
|
AND a.submitted_at >= ?
|
||||||
AND a.submitted_at < ?
|
AND a.submitted_at < ?
|
||||||
ORDER BY s.id, a.sim_id, a.submitted_at ASC",
|
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
|
// Helper: extrahiere "Lektion-Key" (mission/level/scenario) aus assessments-row
|
||||||
@@ -394,10 +394,10 @@ if ($method === 'GET') {
|
|||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
JOIN student_sessions ss ON ss.id = a.session_id
|
||||||
WHERE a.class_id = ? AND a.sim_id = ?
|
WHERE a.class_id = ? AND a.sim_id = ?
|
||||||
AND ss.display_name = ?
|
AND ss.student_id = ?
|
||||||
AND a.submitted_at BETWEEN ? AND ?
|
AND a.submitted_at BETWEEN ? AND ?
|
||||||
ORDER BY a.submitted_at DESC',
|
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]);
|
if (!$rows) Response::ok(['results' => null, 'assessmentCount' => 0]);
|
||||||
// Nimm den Row mit dem reichhaltigsten Results-JSON (Heuristik: hat .stars oder .results.totalStars)
|
// Nimm den Row mit dem reichhaltigsten Results-JSON (Heuristik: hat .stars oder .results.totalStars)
|
||||||
@@ -600,7 +600,7 @@ if ($method === 'GET') {
|
|||||||
$heuteRows = $db->fetchAll(
|
$heuteRows = $db->fetchAll(
|
||||||
"SELECT DISTINCT s.id AS student_id
|
"SELECT DISTINCT s.id AS student_id
|
||||||
FROM students s
|
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 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()
|
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
|
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'),
|
MAX(GREATEST(IFNULL(a.submitted_at, '1970-01-01'),
|
||||||
IFNULL(ls.last_seen, '1970-01-01'))) AS last_seen_any
|
IFNULL(ls.last_seen, '1970-01-01'))) AS last_seen_any
|
||||||
FROM students s
|
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 assessments a ON a.session_id = ss.id
|
||||||
LEFT JOIN live_sessions ls ON ls.student_id = s.id
|
LEFT JOIN live_sessions ls ON ls.student_id = s.id
|
||||||
WHERE s.class_id = ? AND s.deleted_at IS NULL
|
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
|
"SELECT s.id AS student_id, a.sim_id, a.results, a.process_log, a.submitted_at
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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
|
WHERE a.class_id = ? AND s.deleted_at IS NULL
|
||||||
ORDER BY s.id, a.sim_id, a.submitted_at DESC",
|
ORDER BY s.id, a.sim_id, a.submitted_at DESC",
|
||||||
[$classId]
|
[$classId]
|
||||||
@@ -729,7 +729,7 @@ if ($method === 'GET') {
|
|||||||
SUM(COALESCE(a.duration_ms, 0)) / 1000 AS sec
|
SUM(COALESCE(a.duration_ms, 0)) / 1000 AS sec
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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
|
WHERE a.class_id = ? AND $sinceSql AND s.deleted_at IS NULL
|
||||||
GROUP BY bucket, a.sim_id",
|
GROUP BY bucket, a.sim_id",
|
||||||
[$classId]
|
[$classId]
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ if ($method === 'GET') {
|
|||||||
|
|
||||||
// Individuelle Overrides für diesen Schüler
|
// Individuelle Overrides für diesen Schüler
|
||||||
$studentRow = $db->fetchOne(
|
$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]
|
[$sessionId]
|
||||||
);
|
);
|
||||||
$overrides = [];
|
$overrides = [];
|
||||||
|
|||||||
+10
-6
@@ -16,21 +16,25 @@ if ($method === 'GET') {
|
|||||||
// ─── Sub-View: eigene Reflexionen einer Schüler:in ──────────────────
|
// ─── Sub-View: eigene Reflexionen einer Schüler:in ──────────────────
|
||||||
if (($_GET['view'] ?? '') === 'reflections') {
|
if (($_GET['view'] ?? '') === 'reflections') {
|
||||||
if (!$studentSessionId) Response::error('Nur eingeloggte Schüler:innen', 401);
|
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);
|
if (!$session) Response::error('Session nicht gefunden', 404);
|
||||||
// Reflexionen über alle assessments dieser Schüler:in (matched über
|
// student_id (FK) statt fragilem display_name-Join; Fallback für seltene Alt-Sessions ohne FK.
|
||||||
// class_id + display_name, weil verschiedene Sessions denselben
|
$sid = $session['student_id'] ?? null;
|
||||||
// Bearbeiter:innen-Namen tragen können).
|
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(
|
$rows = $db->fetchAll(
|
||||||
"SELECT a.sim_id, a.submitted_at, a.reflections
|
"SELECT a.sim_id, a.submitted_at, a.reflections
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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 a.reflections IS NOT NULL
|
||||||
AND JSON_LENGTH(a.reflections) > 0
|
AND JSON_LENGTH(a.reflections) > 0
|
||||||
ORDER BY a.submitted_at DESC
|
ORDER BY a.submitted_at DESC
|
||||||
LIMIT 50",
|
LIMIT 50",
|
||||||
[$session['class_id'], $session['display_name']]
|
[$sid]
|
||||||
);
|
);
|
||||||
$items = [];
|
$items = [];
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ if ($method === 'GET') {
|
|||||||
|
|
||||||
if ($studentId && $simId) {
|
if ($studentId && $simId) {
|
||||||
$progress = $db->fetchOne('SELECT level, xp, plays, best_stars FROM player_progress WHERE student_id = ? AND sim_id = ?', [$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
|
// Optional: alle Durchgänge mit results-JSON, wenn ?runs=1 → für Schüler-Eigen-Drilldown
|
||||||
$runs = [];
|
$runs = [];
|
||||||
if (!empty($_GET['runs'])) {
|
if (!empty($_GET['runs'])) {
|
||||||
@@ -59,7 +59,7 @@ if ($method === 'GET') {
|
|||||||
"SELECT a.id, a.submitted_at, a.duration_ms, a.results
|
"SELECT a.id, a.submitted_at, a.duration_ms, a.results
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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 a.results IS NOT NULL
|
||||||
AND (
|
AND (
|
||||||
JSON_EXTRACT(a.results, '$.stars') IS NOT NULL
|
JSON_EXTRACT(a.results, '$.stars') IS NOT NULL
|
||||||
@@ -70,7 +70,7 @@ if ($method === 'GET') {
|
|||||||
)
|
)
|
||||||
ORDER BY a.submitted_at DESC
|
ORDER BY a.submitted_at DESC
|
||||||
LIMIT 50",
|
LIMIT 50",
|
||||||
[$student['class_id'], $student['display_name'], $simId]
|
[$studentId, $simId]
|
||||||
);
|
);
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$row['results'] = $row['results'] ? json_decode($row['results'], true) : null;
|
$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
|
"SELECT s.id AS student_id, a.sim_id, a.results, a.process_log
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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
|
WHERE a.class_id = ? AND s.deleted_at IS NULL
|
||||||
ORDER BY s.id, a.sim_id, a.submitted_at DESC",
|
ORDER BY s.id, a.sim_id, a.submitted_at DESC",
|
||||||
[$classId]
|
[$classId]
|
||||||
@@ -387,7 +387,7 @@ if ($view === 'fleiss') {
|
|||||||
COUNT(*) AS sessions
|
COUNT(*) AS sessions
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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
|
WHERE a.class_id = ? AND $sinceSql AND s.deleted_at IS NULL $modSql
|
||||||
GROUP BY s.id, bucket, a.sim_id",
|
GROUP BY s.id, bucket, a.sim_id",
|
||||||
$params
|
$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
|
"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
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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
|
WHERE a.class_id = ? AND $sinceSqlA AND s.deleted_at IS NULL
|
||||||
GROUP BY bucket, s.id",
|
GROUP BY bucket, s.id",
|
||||||
[$classId]
|
[$classId]
|
||||||
@@ -656,10 +656,10 @@ if ($view === 'module') {
|
|||||||
"SELECT a.id, a.submitted_at, a.duration_ms, a.results, a.reflections
|
"SELECT a.id, a.submitted_at, a.duration_ms, a.results, a.reflections
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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
|
ORDER BY a.submitted_at DESC
|
||||||
LIMIT 100",
|
LIMIT 100",
|
||||||
[$classId, $student['display_name'], $moduleId]
|
[$studentId, $moduleId]
|
||||||
);
|
);
|
||||||
// results + reflections JSON parsen (Frontend sieht's lieber dekodiert)
|
// results + reflections JSON parsen (Frontend sieht's lieber dekodiert)
|
||||||
foreach ($runs as &$run) {
|
foreach ($runs as &$run) {
|
||||||
@@ -851,7 +851,7 @@ if ($view === 'reflections') {
|
|||||||
ss.display_name, s.id AS student_id, s.emoji_avatar
|
ss.display_name, s.id AS student_id, s.emoji_avatar
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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 = ?
|
WHERE a.class_id = ?
|
||||||
AND a.reflections IS NOT NULL
|
AND a.reflections IS NOT NULL
|
||||||
AND JSON_LENGTH(a.reflections) > 0";
|
AND JSON_LENGTH(a.reflections) > 0";
|
||||||
|
|||||||
@@ -76,8 +76,7 @@ class Benchmark
|
|||||||
"SELECT a.sim_id, a.results, a.process_log, a.submitted_at
|
"SELECT a.sim_id, a.results, a.process_log, a.submitted_at
|
||||||
FROM assessments a
|
FROM assessments a
|
||||||
JOIN student_sessions ss ON ss.id = a.session_id
|
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 ss.student_id = ? AND a.class_id = ?
|
||||||
WHERE s.id = ? AND a.class_id = ?
|
|
||||||
ORDER BY a.sim_id, a.submitted_at DESC",
|
ORDER BY a.sim_id, a.submitted_at DESC",
|
||||||
[$studentId, $classId]
|
[$studentId, $classId]
|
||||||
);
|
);
|
||||||
|
|||||||
+13
-4
@@ -20,8 +20,10 @@ class Session {
|
|||||||
return $_COOKIE[self::COOKIE_NAME] ?? null;
|
return $_COOKIE[self::COOKIE_NAME] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Neue Schueler-Session erstellen */
|
/** Neue Schueler-Session erstellen. student_id wird direkt gesetzt (per FK
|
||||||
public static function createStudent(int $classId, string $displayName = ''): string {
|
* 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',
|
$uuid = sprintf('%s-%s-%s-%s-%s',
|
||||||
bin2hex(random_bytes(4)),
|
bin2hex(random_bytes(4)),
|
||||||
bin2hex(random_bytes(2)),
|
bin2hex(random_bytes(2)),
|
||||||
@@ -31,9 +33,16 @@ class Session {
|
|||||||
);
|
);
|
||||||
|
|
||||||
$db = Database::get();
|
$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(
|
$db->execute(
|
||||||
'INSERT INTO student_sessions (id, class_id, display_name) VALUES (?, ?, ?)',
|
'INSERT INTO student_sessions (id, class_id, display_name, student_id) VALUES (?, ?, ?, ?)',
|
||||||
[$uuid, $classId, $displayName]
|
[$uuid, $classId, $displayName, $studentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
setcookie(self::COOKIE_NAME, $uuid, [
|
setcookie(self::COOKIE_NAME, $uuid, [
|
||||||
|
|||||||
Reference in New Issue
Block a user