refactor: split classroomController.js into 7 domain files (phase 2 of 2)
1618-line monolith split into: classroom/_shared.js — GUEST_EVENTS, emitToSession, hasAccess, canDraw classroom/sessions.js — lifecycle + guest tokens (12 functions) classroom/strokes.js — CRUD + cursor + preview (7 functions) classroom/pages.js — page CRUD + theme (8 functions) classroom/chat.js — messages, reactions, attachments, export (7 functions) classroom/permissions.js — draw, hand, mute, screen, attendance (11 functions) classroom/sim.js — simulation relay (5 functions) classroom/admin.js — history, notes, templates, admin views (14 functions) classroomController.js is now a 9-line re-export facade. routes/classroom.js unchanged. All 65 exports verified. Tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
'use strict';
|
||||
const db = require('../../db/db');
|
||||
const { emitToSession, hasAccess } = require('./_shared');
|
||||
|
||||
function getNotes(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const row = db.prepare('SELECT content FROM classroom_notes WHERE session_id=? AND user_id=?').get(sessionId, req.user.id);
|
||||
res.json({ content: row?.content || '' });
|
||||
}
|
||||
|
||||
function saveNotes(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const { content = '' } = req.body;
|
||||
db.prepare(`
|
||||
INSERT INTO classroom_notes (session_id, user_id, content, updated_at)
|
||||
VALUES (?,?,?,datetime('now'))
|
||||
ON CONFLICT(session_id, user_id) DO UPDATE SET content=excluded.content, updated_at=excluded.updated_at
|
||||
`).run(sessionId, req.user.id, content.slice(0, 50000));
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
function getClassHistory(req, res) {
|
||||
const classId = Number(req.params.classId);
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const limit = Math.min(50, Number(req.query.limit) || 20);
|
||||
const search = (req.query.search || '').trim();
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const cls = db.prepare('SELECT * FROM classes WHERE id=?').get(classId);
|
||||
if (!cls) return res.status(404).json({ error: 'Класс не найден' });
|
||||
if (req.user.role === 'teacher') {
|
||||
if (cls.teacher_id !== req.user.id) return res.status(403).json({ error: 'Нет доступа' });
|
||||
} else if (req.user.role === 'student' || req.user.role === 'free_student') {
|
||||
const member = db.prepare('SELECT 1 FROM class_members WHERE class_id=? AND user_id=?').get(classId, req.user.id);
|
||||
if (!member) return res.status(403).json({ error: 'Нет доступа' });
|
||||
}
|
||||
|
||||
const whereSearch = search ? `AND (s.title LIKE '%'||?||'%')` : '';
|
||||
const params = search ? [classId, search] : [classId];
|
||||
const total = db.prepare(`SELECT COUNT(*) AS n FROM classroom_sessions s WHERE s.class_id=? AND s.status='ended' ${whereSearch}`).get(...params).n;
|
||||
const sessions = db.prepare(`
|
||||
SELECT s.id, s.title, s.created_at, s.ended_at, s.teacher_id,
|
||||
u.name AS teacher_name,
|
||||
(SELECT COUNT(*) FROM classroom_attendance WHERE session_id=s.id) AS participant_count,
|
||||
(SELECT COUNT(*) FROM classroom_chat WHERE session_id=s.id) AS message_count,
|
||||
(COALESCE((SELECT MAX(page_num) FROM classroom_pages WHERE session_id=s.id),1)) AS page_count,
|
||||
(SELECT COUNT(*) FROM classroom_strokes WHERE session_id=s.id) AS stroke_count
|
||||
FROM classroom_sessions s JOIN users u ON u.id = s.teacher_id
|
||||
WHERE s.class_id=? AND s.status='ended' ${whereSearch}
|
||||
ORDER BY s.ended_at DESC LIMIT ? OFFSET ?
|
||||
`).all(...params, limit, offset);
|
||||
res.json({ sessions, total, page, pages: Math.ceil(total / limit) });
|
||||
}
|
||||
|
||||
function getMyHistory(req, res) {
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const limit = Math.min(50, Number(req.query.limit) || 20);
|
||||
const offset = (page - 1) * limit;
|
||||
let sessions, total;
|
||||
|
||||
if (req.user.role === 'teacher' || req.user.role === 'admin') {
|
||||
total = db.prepare(`SELECT COUNT(*) AS n FROM classroom_sessions WHERE teacher_id=? AND status='ended'`).get(req.user.id).n;
|
||||
sessions = db.prepare(`
|
||||
SELECT s.id, s.title, s.created_at, s.ended_at, s.class_id, s.teacher_id,
|
||||
u.name AS teacher_name, c.name AS class_name,
|
||||
(SELECT COUNT(*) FROM classroom_attendance WHERE session_id=s.id) AS participant_count,
|
||||
(SELECT COUNT(*) FROM classroom_chat WHERE session_id=s.id) AS message_count,
|
||||
(COALESCE((SELECT MAX(page_num) FROM classroom_pages WHERE session_id=s.id),1)) AS page_count
|
||||
FROM classroom_sessions s JOIN users u ON u.id = s.teacher_id
|
||||
LEFT JOIN classes c ON c.id = s.class_id
|
||||
WHERE s.teacher_id=? AND s.status='ended' ORDER BY s.ended_at DESC LIMIT ? OFFSET ?
|
||||
`).all(req.user.id, limit, offset);
|
||||
} else {
|
||||
total = db.prepare(`SELECT COUNT(DISTINCT s.id) AS n FROM classroom_sessions s
|
||||
JOIN classroom_attendance a ON a.session_id=s.id WHERE a.user_id=? AND s.status='ended'`).get(req.user.id).n;
|
||||
sessions = db.prepare(`
|
||||
SELECT s.id, s.title, s.created_at, s.ended_at, s.class_id,
|
||||
u.name AS teacher_name, c.name AS class_name,
|
||||
(SELECT COUNT(*) FROM classroom_attendance WHERE session_id=s.id) AS participant_count,
|
||||
(SELECT COUNT(*) FROM classroom_chat WHERE session_id=s.id) AS message_count,
|
||||
(COALESCE((SELECT MAX(page_num) FROM classroom_pages WHERE session_id=s.id),1)) AS page_count
|
||||
FROM classroom_sessions s JOIN users u ON u.id = s.teacher_id
|
||||
LEFT JOIN classes c ON c.id = s.class_id
|
||||
JOIN classroom_attendance a ON a.session_id=s.id
|
||||
WHERE a.user_id=? AND s.status='ended'
|
||||
GROUP BY s.id ORDER BY s.ended_at DESC LIMIT ? OFFSET ?
|
||||
`).all(req.user.id, limit, offset);
|
||||
}
|
||||
res.json({ sessions, total, page, pages: Math.ceil(total / limit) });
|
||||
}
|
||||
|
||||
function getSessionSummary(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId);
|
||||
if (!session) return res.status(404).json({ error: 'Не найдено' });
|
||||
if (!hasAccess(session, req.user.id, req.user.role)) return res.status(403).json({ error: 'Нет доступа' });
|
||||
|
||||
const teacherName = db.prepare('SELECT name FROM users WHERE id=?').get(session.teacher_id)?.name || '';
|
||||
const className = session.class_id ? db.prepare('SELECT name FROM classes WHERE id=?').get(session.class_id)?.name || null : null;
|
||||
const durationSec = session.ended_at ? Math.round((new Date(session.ended_at) - new Date(session.created_at)) / 1000) : null;
|
||||
|
||||
const stats = {
|
||||
duration_sec: durationSec,
|
||||
participant_count: db.prepare('SELECT COUNT(*) AS n FROM classroom_attendance WHERE session_id=?').get(sessionId).n,
|
||||
message_count: db.prepare('SELECT COUNT(*) AS n FROM classroom_chat WHERE session_id=?').get(sessionId).n,
|
||||
page_count: db.prepare('SELECT COALESCE(MAX(page_num),1) AS n FROM classroom_pages WHERE session_id=?').get(sessionId).n,
|
||||
stroke_count: db.prepare('SELECT COUNT(*) AS n FROM classroom_strokes WHERE session_id=?').get(sessionId).n,
|
||||
};
|
||||
|
||||
const attendance = db.prepare(`
|
||||
SELECT a.user_id, u.name AS user_name, a.joined_at, a.left_at
|
||||
FROM classroom_attendance a JOIN users u ON u.id = a.user_id
|
||||
WHERE a.session_id=? ORDER BY a.joined_at
|
||||
`).all(sessionId).map(r => ({ ...r, duration_sec: (r.joined_at && r.left_at) ? Math.round((new Date(r.left_at) - new Date(r.joined_at)) / 1000) : null }));
|
||||
|
||||
const pages = db.prepare(`
|
||||
SELECT p.page_num, p.template, p.name,
|
||||
(SELECT COUNT(*) FROM classroom_strokes WHERE session_id=p.session_id AND page_num=p.page_num) AS stroke_count
|
||||
FROM classroom_pages p WHERE p.session_id=? ORDER BY p.page_num
|
||||
`).all(sessionId);
|
||||
if (!pages.length) pages.push({ page_num: 1, template: 'blank', name: null, stroke_count: stats.stroke_count });
|
||||
|
||||
res.json({ session: { ...session, teacher_name: teacherName, class_name: className }, stats, attendance, pages });
|
||||
}
|
||||
|
||||
function getAllNotes(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId);
|
||||
if (!session) return res.status(404).json({ error: 'Не найдено' });
|
||||
if (session.teacher_id !== req.user.id && req.user.role !== 'admin')
|
||||
return res.status(403).json({ error: 'Нет доступа' });
|
||||
|
||||
const notes = db.prepare(`
|
||||
SELECT n.content, n.updated_at, u.name AS user_name, u.id AS user_id
|
||||
FROM classroom_notes n JOIN users u ON u.id = n.user_id
|
||||
WHERE n.session_id=? AND n.content != '' ORDER BY u.name
|
||||
`).all(sessionId);
|
||||
res.json({ notes });
|
||||
}
|
||||
|
||||
function deleteHistorySession(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId);
|
||||
if (!session) return res.status(404).json({ error: 'Не найдено' });
|
||||
if (session.status !== 'ended') return res.status(400).json({ error: 'Можно удалять только завершённые сессии' });
|
||||
if (session.teacher_id !== req.user.id && req.user.role !== 'admin')
|
||||
return res.status(403).json({ error: 'Нет доступа' });
|
||||
|
||||
db.transaction(() => {
|
||||
db.prepare('DELETE FROM classroom_chat_reactions WHERE chat_id IN (SELECT id FROM classroom_chat WHERE session_id=?)').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_chat WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_strokes WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_pages WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_attendance WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_notes WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_draw_permissions WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_hands WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_invites WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_sessions WHERE id=?').run(sessionId);
|
||||
})();
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
function adminGetActiveSessions(req, res) {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Нет доступа' });
|
||||
const sessions = db.prepare(`
|
||||
SELECT s.id, s.title, s.created_at, s.class_id, s.teacher_id,
|
||||
u.name AS teacher_name, c.name AS class_name,
|
||||
(SELECT COUNT(*) FROM classroom_attendance WHERE session_id=s.id AND left_at IS NULL) AS online_count,
|
||||
(SELECT COUNT(*) FROM classroom_attendance WHERE session_id=s.id) AS total_joined,
|
||||
(SELECT COUNT(*) FROM classroom_chat WHERE session_id=s.id) AS message_count
|
||||
FROM classroom_sessions s JOIN users u ON u.id = s.teacher_id
|
||||
LEFT JOIN classes c ON c.id = s.class_id WHERE s.status='active' ORDER BY s.created_at DESC
|
||||
`).all();
|
||||
res.json({ sessions });
|
||||
}
|
||||
|
||||
function adminGetAllSessions(req, res) {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Нет доступа' });
|
||||
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const limit = Math.min(50, Number(req.query.limit) || 20);
|
||||
const search = (req.query.search || '').trim();
|
||||
const teacherId = (req.query.teacher || '').trim();
|
||||
const classId = (req.query.class_id || '').trim();
|
||||
const dateFrom = (req.query.date_from || '').trim();
|
||||
const dateTo = (req.query.date_to || '').trim();
|
||||
const sort = req.query.sort || 'newest';
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let where = "s.status='ended'";
|
||||
const params = [];
|
||||
if (search) { where += ` AND (s.title LIKE '%'||?||'%' OR u.name LIKE '%'||?||'%' OR c.name LIKE '%'||?||'%')`; params.push(search, search, search); }
|
||||
if (teacherId) { where += ` AND s.teacher_id=?`; params.push(Number(teacherId)); }
|
||||
if (classId) { where += ` AND s.class_id=?`; params.push(Number(classId)); }
|
||||
if (dateFrom) { where += ` AND date(s.created_at)>=?`; params.push(dateFrom); }
|
||||
if (dateTo) { where += ` AND date(s.created_at)<=?`; params.push(dateTo); }
|
||||
|
||||
const orderBy = sort === 'oldest' ? 's.ended_at ASC'
|
||||
: sort === 'longest' ? '(julianday(s.ended_at)-julianday(s.created_at)) DESC'
|
||||
: sort === 'most_students' ? 'participant_count DESC'
|
||||
: 's.ended_at DESC';
|
||||
|
||||
const total = db.prepare(`SELECT COUNT(*) AS n FROM classroom_sessions s
|
||||
JOIN users u ON u.id = s.teacher_id LEFT JOIN classes c ON c.id = s.class_id WHERE ${where}`).get(...params).n;
|
||||
|
||||
const sessions = db.prepare(`
|
||||
SELECT s.id, s.title, s.created_at, s.ended_at, s.class_id, s.teacher_id,
|
||||
u.name AS teacher_name, c.name AS class_name,
|
||||
(SELECT COUNT(*) FROM classroom_attendance WHERE session_id=s.id) AS participant_count,
|
||||
(SELECT COUNT(*) FROM classroom_chat WHERE session_id=s.id) AS message_count,
|
||||
COALESCE((SELECT MAX(page_num) FROM classroom_pages WHERE session_id=s.id),1) AS page_count,
|
||||
(SELECT COUNT(*) FROM classroom_strokes WHERE session_id=s.id) AS stroke_count
|
||||
FROM classroom_sessions s JOIN users u ON u.id = s.teacher_id
|
||||
LEFT JOIN classes c ON c.id = s.class_id
|
||||
WHERE ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?
|
||||
`).all(...params, limit, offset);
|
||||
|
||||
const agg = db.prepare(`
|
||||
SELECT COUNT(*) AS total_sessions, COUNT(DISTINCT s.teacher_id) AS total_teachers,
|
||||
COALESCE(SUM(CASE WHEN s.ended_at IS NOT NULL AND s.created_at IS NOT NULL
|
||||
THEN CAST((julianday(s.ended_at) - julianday(s.created_at)) * 86400 AS INTEGER)
|
||||
ELSE 0 END), 0) AS total_duration_sec
|
||||
FROM classroom_sessions s JOIN users u ON u.id = s.teacher_id
|
||||
LEFT JOIN classes c ON c.id = s.class_id WHERE ${where}
|
||||
`).get(...params);
|
||||
|
||||
const aggPart = db.prepare(`
|
||||
SELECT COALESCE(SUM(sub.pc),0) AS total_participants, COALESCE(SUM(sub.mc),0) AS total_messages
|
||||
FROM (
|
||||
SELECT s.id,
|
||||
(SELECT COUNT(*) FROM classroom_attendance WHERE session_id=s.id) AS pc,
|
||||
(SELECT COUNT(*) FROM classroom_chat WHERE session_id=s.id) AS mc
|
||||
FROM classroom_sessions s JOIN users u ON u.id = s.teacher_id
|
||||
LEFT JOIN classes c ON c.id = s.class_id WHERE ${where}
|
||||
) sub
|
||||
`).get(...params);
|
||||
|
||||
res.json({ sessions, total, page, pages: Math.ceil(total / limit), agg: { ...agg, ...aggPart } });
|
||||
}
|
||||
|
||||
function adminGetTeachersList(req, res) {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Нет доступа' });
|
||||
const teachers = db.prepare(`
|
||||
SELECT DISTINCT u.id, u.name FROM classroom_sessions s
|
||||
JOIN users u ON u.id = s.teacher_id WHERE s.status='ended' ORDER BY u.name
|
||||
`).all();
|
||||
res.json({ teachers });
|
||||
}
|
||||
|
||||
function getTemplates(req, res) {
|
||||
const templates = db.prepare(
|
||||
'SELECT id, title, description, created_at FROM classroom_templates WHERE teacher_id=? ORDER BY created_at DESC'
|
||||
).all(req.user.id);
|
||||
res.json({ templates });
|
||||
}
|
||||
|
||||
function saveTemplate(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const { title, description = '' } = req.body;
|
||||
if (!title?.trim()) return res.status(400).json({ error: 'Укажите название шаблона' });
|
||||
|
||||
const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId);
|
||||
if (!session) return res.status(404).json({ error: 'Сессия не найдена' });
|
||||
if (session.teacher_id !== req.user.id && req.user.role !== 'admin')
|
||||
return res.status(403).json({ error: 'Нет доступа' });
|
||||
|
||||
const pages = db.prepare('SELECT * FROM classroom_pages WHERE session_id=? ORDER BY page_num').all(sessionId);
|
||||
const pagesData = pages.map(p => {
|
||||
const strokes = db.prepare('SELECT tool, data FROM classroom_strokes WHERE session_id=? AND page_num=? ORDER BY seq').all(sessionId, p.page_num);
|
||||
return { page_num: p.page_num, template: p.template || 'blank', strokes: strokes.map(s => ({ tool: s.tool, data: JSON.parse(s.data) })) };
|
||||
});
|
||||
|
||||
const { lastInsertRowid } = db.prepare(
|
||||
'INSERT INTO classroom_templates (teacher_id, title, description, pages_data) VALUES (?,?,?,?)'
|
||||
).run(req.user.id, title.trim(), description.slice(0, 500), JSON.stringify(pagesData));
|
||||
res.json({ id: lastInsertRowid, ok: true });
|
||||
}
|
||||
|
||||
function deleteTemplate(req, res) {
|
||||
const id = Number(req.params.tid);
|
||||
db.prepare('DELETE FROM classroom_templates WHERE id=? AND teacher_id=?').run(id, req.user.id);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
function loadTemplate(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const { template_id } = req.body;
|
||||
const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId);
|
||||
if (!session) return res.status(404).json({ error: 'Сессия не активна' });
|
||||
if (session.teacher_id !== req.user.id && req.user.role !== 'admin')
|
||||
return res.status(403).json({ error: 'Нет доступа' });
|
||||
|
||||
const tmpl = db.prepare('SELECT * FROM classroom_templates WHERE id=? AND teacher_id=?').get(template_id, req.user.id);
|
||||
if (!tmpl && req.user.role !== 'admin') return res.status(404).json({ error: 'Шаблон не найден' });
|
||||
const tmplFallback = tmpl || db.prepare('SELECT * FROM classroom_templates WHERE id=?').get(template_id);
|
||||
if (!tmplFallback) return res.status(404).json({ error: 'Шаблон не найден' });
|
||||
|
||||
const pagesData = JSON.parse(tmplFallback.pages_data || '[]');
|
||||
db.prepare('DELETE FROM classroom_strokes WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_pages WHERE session_id=?').run(sessionId);
|
||||
pagesData.forEach(p => {
|
||||
db.prepare('INSERT OR IGNORE INTO classroom_pages (session_id, page_num, template) VALUES (?,?,?)').run(sessionId, p.page_num, p.template || 'blank');
|
||||
(p.strokes || []).forEach(s => {
|
||||
db.prepare('INSERT INTO classroom_strokes (session_id, page_num, tool, data) VALUES (?,?,?,?)').run(sessionId, p.page_num, s.tool, JSON.stringify(s.data));
|
||||
});
|
||||
});
|
||||
|
||||
emitToSession(sessionId, { type: 'classroom_template_loaded', sessionId, pages: pagesData.length });
|
||||
res.json({ ok: true, pages: pagesData.length });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNotes, saveNotes, getAllNotes,
|
||||
getClassHistory, getMyHistory, getSessionSummary, deleteHistorySession,
|
||||
adminGetActiveSessions, adminGetAllSessions, adminGetTeachersList,
|
||||
getTemplates, saveTemplate, deleteTemplate, loadTemplate,
|
||||
};
|
||||
Reference in New Issue
Block a user