Разработка системы тестов и квизов для LMS

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка системы тестов и квизов для LMS
Средняя
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка системы тестов и квизов в LMS

Тесты в LMS — это не просто вопросы и ответы. Нужны: разные типы вопросов, перемешивание, ограничение времени, несколько попыток, детальный результат с правильными ответами, защита от списывания.

Типы вопросов

type QuestionType =
  | 'single_choice'      // один правильный ответ
  | 'multiple_choice'    // несколько правильных
  | 'true_false'
  | 'short_answer'       // текст, проверяется по ключевым словам или вручную
  | 'ordering'           // расставить в правильном порядке
  | 'matching'           // соответствие пар
  | 'fill_blank';        // заполнить пропуск

interface Question {
  id: string;
  type: QuestionType;
  text: string;
  points: number;
  explanation?: string;   // показывается после ответа
  answers: Answer[];
  // Для fill_blank
  blanks?: string[];
  // Для matching
  pairs?: Array<{ left: string; right: string }>;
}

interface QuizSettings {
  timeLimit?: number;       // секунды, null = без ограничения
  maxAttempts: number;      // 0 = неограниченно
  passingScore: number;     // процент
  shuffleQuestions: boolean;
  shuffleAnswers: boolean;
  showCorrectAnswers: 'never' | 'after_attempt' | 'after_passing';
  allowReview: boolean;     // можно вернуться к предыдущим вопросам
}

Начало попытки

app.post('/api/quizzes/:quizId/attempts', authenticate, async (req, res) => {
  const quiz = await db.quizzes.findById(req.params.quizId);
  const enrollment = await db.enrollments.findByUserAndCourse(req.user.id, quiz.courseId);

  // Проверить лимит попыток
  const previousAttempts = await db.quizAttempts.countByUserAndQuiz(
    req.user.id, quiz.id
  );

  if (quiz.settings.maxAttempts > 0 && previousAttempts >= quiz.settings.maxAttempts) {
    return res.status(429).json({ error: 'Max attempts reached' });
  }

  // Перемешать вопросы
  let questions = quiz.questions;
  if (quiz.settings.shuffleQuestions) {
    questions = shuffleArray([...questions]);
  }

  if (quiz.settings.shuffleAnswers) {
    questions = questions.map(q => ({
      ...q,
      answers: q.type !== 'ordering' ? shuffleArray([...q.answers]) : q.answers,
    }));
  }

  const attempt = await db.quizAttempts.create({
    quizId: quiz.id,
    userId: req.user.id,
    enrollmentId: enrollment.id,
    questions: questions.map(q => ({
      id: q.id,
      // НЕ отправляем isCorrect ответов клиенту
      answers: q.answers.map(a => ({ id: a.id, text: a.text })),
    })),
    startedAt: new Date(),
    expiresAt: quiz.settings.timeLimit
      ? new Date(Date.now() + quiz.settings.timeLimit * 1000)
      : null,
  });

  res.json({
    attemptId: attempt.id,
    questions: attempt.questions,
    expiresAt: attempt.expiresAt,
  });
});

Отправка ответов и проверка

app.post('/api/attempts/:attemptId/submit', authenticate, async (req, res) => {
  const attempt = await db.quizAttempts.findById(req.params.attemptId);

  if (attempt.userId !== req.user.id) return res.status(403).end();
  if (attempt.submittedAt) return res.status(409).json({ error: 'Already submitted' });

  // Проверить таймер
  if (attempt.expiresAt && new Date() > attempt.expiresAt) {
    return res.status(408).json({ error: 'Time expired' });
  }

  const { answers } = req.body;  // { questionId: answerId | answerId[] | string }

  const quiz = await db.quizzes.findById(attempt.quizId);
  let totalPoints = 0;
  let earnedPoints = 0;

  const results = quiz.questions.map(question => {
    totalPoints += question.points;
    const userAnswer = answers[question.id];
    let isCorrect = false;
    let pointsEarned = 0;

    switch (question.type) {
      case 'single_choice':
      case 'true_false':
        const correctAnswer = question.answers.find(a => a.isCorrect);
        isCorrect = userAnswer === correctAnswer?.id;
        pointsEarned = isCorrect ? question.points : 0;
        break;

      case 'multiple_choice':
        const correctIds = new Set(question.answers.filter(a => a.isCorrect).map(a => a.id));
        const userIds = new Set(Array.isArray(userAnswer) ? userAnswer : []);
        isCorrect = correctIds.size === userIds.size &&
          [...correctIds].every(id => userIds.has(id));
        // Частичные баллы за множественный выбор
        const correctSelected = [...userIds].filter(id => correctIds.has(id)).length;
        const wrongSelected = [...userIds].filter(id => !correctIds.has(id)).length;
        pointsEarned = Math.max(0,
          (correctSelected / correctIds.size - wrongSelected / correctIds.size) * question.points
        );
        break;

      case 'ordering':
        const correctOrder = question.answers.map(a => a.id);
        isCorrect = JSON.stringify(userAnswer) === JSON.stringify(correctOrder);
        pointsEarned = isCorrect ? question.points : 0;
        break;

      case 'short_answer':
        // Автопроверка по ключевым словам, ручная — отдельно
        const keywords = question.answers[0]?.keywords ?? [];
        const matchCount = keywords.filter(kw =>
          (userAnswer as string).toLowerCase().includes(kw.toLowerCase())
        ).length;
        isCorrect = matchCount >= (question.answers[0]?.minKeywords ?? 1);
        pointsEarned = isCorrect ? question.points : 0;
        break;
    }

    earnedPoints += pointsEarned;
    return { questionId: question.id, isCorrect, pointsEarned, correctAnswer: question.answers.filter(a => a.isCorrect) };
  });

  const scorePercent = Math.round((earnedPoints / totalPoints) * 100);
  const passed = scorePercent >= quiz.settings.passingScore;

  await db.quizAttempts.update(attempt.id, {
    answers,
    results,
    score: scorePercent,
    passed,
    submittedAt: new Date(),
    timeSpent: Math.round((Date.now() - new Date(attempt.startedAt).getTime()) / 1000),
  });

  if (passed) {
    await db.lessonProgress.markCompleted(req.user.id, quiz.lessonId);
  }

  const showAnswers = quiz.settings.showCorrectAnswers === 'after_attempt' ||
    (quiz.settings.showCorrectAnswers === 'after_passing' && passed);

  res.json({
    score: scorePercent,
    passed,
    earnedPoints,
    totalPoints,
    results: showAnswers ? results : results.map(r => ({ questionId: r.questionId, isCorrect: r.isCorrect })),
  });
});

Таймер на клиенте

function QuizTimer({ expiresAt, onExpired }) {
  const [remaining, setRemaining] = useState(0);

  useEffect(() => {
    const update = () => {
      const diff = Math.max(0, new Date(expiresAt).getTime() - Date.now());
      setRemaining(Math.floor(diff / 1000));
      if (diff <= 0) onExpired();
    };

    update();
    const timer = setInterval(update, 1000);
    return () => clearInterval(timer);
  }, [expiresAt]);

  const minutes = Math.floor(remaining / 60);
  const seconds = remaining % 60;
  const isUrgent = remaining < 60;

  return (
    <div className={`font-mono text-lg font-bold ${isUrgent ? 'text-red-600 animate-pulse' : 'text-gray-700'}`}>
      {String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
    </div>
  );
}

Сроки

Система тестов с 4–5 типами вопросов, таймером, попытками и результатами — 1–1.5 недели.