考试和测验系统

AI Content Team
20 分钟阅读

设计和实现在线考试、随堂测验以及自适应测试系统

考试和测验系统

有效的评估工具能够准确测量学生的学习成果。本文讲解完整的考试系统设计。

测验类型设计

多种题目类型

const questionTypes = {
  MULTIPLE_CHOICE: {
    id: 'multiple_choice',
    name: '多选题',
    fields: {
      question: '题目',
      options: ['选项 A', '选项 B', '选项 C', '选项 D'],
      correctAnswer: 0,
      explanation: '解释',
    },
  },
  
  TRUE_FALSE: {
    id: 'true_false',
    name: '判断题',
    fields: {
      question: '陈述',
      correctAnswer: true,
    },
  },
  
  SHORT_ANSWER: {
    id: 'short_answer',
    name: '简答题',
    fields: {
      question: '题目',
      acceptableAnswers: ['答案1', '答案2'],
      fuzzyMatching: true, // 允许部分匹配
    },
  },
  
  ESSAY: {
    id: 'essay',
    name: '论文题',
    fields: {
      question: '题目',
      maxWords: 500,
      rubric: ['结构', '内容', '语言'],
    },
  },
  
  MATCHING: {
    id: 'matching',
    name: '配对题',
    fields: {
      pairs: [
        { left: '项目 A', right: '定义 1' },
        { left: '项目 B', right: '定义 2' },
      ],
    },
  },
  
  FILL_BLANK: {
    id: 'fill_blank',
    name: '填空题',
    fields: {
      text: '这是一个 _____ 的例子',
      blanks: [{ answer: '正确', alternatives: ['对', 'OK'] }],
    },
  },
};

const quizQuestion = {
  id: 'Q001',
  type: 'multiple_choice',
  question: 'JavaScript 中 var 和 let 的区别是什么?',
  options: [
    '没有区别',
    'let 有块级作用域,var 没有',
    'var 有块级作用域,let 没有',
    'let 和 var 都没有块级作用域',
  ],
  correctAnswer: 1,
  points: 5,
  explanation: 'let 在块级作用域内,而 var 只有函数作用域',
  difficulty: 'medium', // easy, medium, hard
};

测验创建界面

教师创建测验

function CreateQuiz() {
  const [quizData, setQuizData] = useState({
    title: 'Web 基础测试',
    description: '测试你对 Web 基础的理解',
    timeLimit: 60, // 分钟
    totalPoints: 100,
    passingScore: 70,
    shuffleQuestions: true,
    shuffleOptions: true,
    allowReview: true,
    showCorrectAnswers: 'after_quiz', // immediate, after_quiz, never
  });
  
  const [questions, setQuestions] = useState([]);
  const [currentQuestion, setCurrentQuestion] = useState(null);
  
  const addQuestion = (question) => {
    setQuestions([...questions, {
      ...question,
      id: `Q_${Date.now()}`,
    }]);
  };
  
  const handleCreateQuiz = async () => {
    const quiz = {
      ...quizData,
      questions,
      totalPoints: questions.reduce((sum, q) => sum + q.points, 0),
    };
    
    try {
      const response = await api.post('/quizzes', quiz);
      console.log('Quiz created:', response);
    } catch (error) {
      console.error('Failed to create quiz:', error);
    }
  };
  
  return (
    <div className="quiz-creator">
      <section className="quiz-settings">
        <input
          type="text"
          value={quizData.title}
          onChange={(e) => setQuizData({...quizData, title: e.target.value})}
          placeholder="测验标题"
        />
        
        <textarea
          value={quizData.description}
          onChange={(e) => setQuizData({...quizData, description: e.target.value})}
          placeholder="测验描述"
        />
        
        <div className="settings-grid">
          <input
            type="number"
            value={quizData.timeLimit}
            onChange={(e) => setQuizData({...quizData, timeLimit: parseInt(e.target.value)})}
            placeholder="时间限制 (分钟)"
          />
          
          <input
            type="number"
            value={quizData.passingScore}
            onChange={(e) => setQuizData({...quizData, passingScore: parseInt(e.target.value)})}
            placeholder="及格分数"
          />
          
          <select
            value={quizData.showCorrectAnswers}
            onChange={(e) => setQuizData({...quizData, showCorrectAnswers: e.target.value})}
          >
            <option value="immediate">立即显示答案</option>
            <option value="after_quiz">测验后显示答案</option>
            <option value="never">不显示答案</option>
          </select>
        </div>
        
        <label>
          <input
            type="checkbox"
            checked={quizData.shuffleQuestions}
            onChange={(e) => setQuizData({...quizData, shuffleQuestions: e.target.checked})}
          />
          随机打乱题目顺序
        </label>
      </section>
      
      <section className="questions-section">
        <h2>题目 ({questions.length})</h2>
        
        {questions.map((q, idx) => (
          <div key={q.id} className="question-preview">
            <span className="question-number">{idx + 1}</span>
            <span>{q.question}</span>
            <span className="points">{q.points} 分</span>
          </div>
        ))}
        
        <button onClick={() => setCurrentQuestion({})}>
          添加题目
        </button>
      </section>
      
      {currentQuestion !== null && (
        <QuestionEditor
          question={currentQuestion}
          onSave={addQuestion}
          onCancel={() => setCurrentQuestion(null)}
        />
      )}
      
      <button onClick={handleCreateQuiz} className="primary">
        创建测验
      </button>
    </div>
  );
}

学生参加测验

在线测验界面

function TakeQuiz() {
  const { quizId } = useParams();
  const [quiz, setQuiz] = useState(null);
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
  const [responses, setResponses] = useState({});
  const [timeRemaining, setTimeRemaining] = useState(null);
  const [submitted, setSubmitted] = useState(false);
  
  useEffect(() => {
    fetchQuiz(quizId).then(quiz => {
      setQuiz(quiz);
      setTimeRemaining(quiz.timeLimit * 60); // 转换为秒
    });
  }, [quizId]);
  
  // 倒计时
  useEffect(() => {
    if (timeRemaining === null || submitted) return;
    
    const interval = setInterval(() => {
      setTimeRemaining(t => t - 1);
      
      if (timeRemaining <= 0) {
        handleSubmit();
      }
    }, 1000);
    
    return () => clearInterval(interval);
  }, [timeRemaining, submitted]);
  
  const currentQuestion = quiz?.questions[currentQuestionIndex];
  
  const handleAnswerChange = (answer) => {
    setResponses({
      ...responses,
      [currentQuestion.id]: answer,
    });
  };
  
  const handleNext = () => {
    if (currentQuestionIndex < quiz.questions.length - 1) {
      setCurrentQuestionIndex(currentQuestionIndex + 1);
    }
  };
  
  const handlePrevious = () => {
    if (currentQuestionIndex > 0) {
      setCurrentQuestionIndex(currentQuestionIndex - 1);
    }
  };
  
  const handleSubmit = async () => {
    if (!window.confirm('你确定要提交测验吗?')) return;
    
    try {
      const submission = {
        quizId,
        responses,
        submittedAt: new Date().toISOString(),
        timeSpent: (quiz.timeLimit * 60) - timeRemaining,
      };
      
      const result = await api.post(`/quizzes/${quizId}/submit`, submission);
      setSubmitted(true);
      // 显示结果或重定向
    } catch (error) {
      console.error('Failed to submit quiz:', error);
    }
  };
  
  if (!quiz) return <div>加载中...</div>;
  
  const formatTime = (seconds) => {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = seconds % 60;
    return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  };
  
  return (
    <div className="quiz-container">
      <div className="quiz-header">
        <h1>{quiz.title}</h1>
        <div className="timer" style={{
          color: timeRemaining < 300 ? 'red' : 'black'
        }}>
          ⏱️ {formatTime(timeRemaining)}
        </div>
      </div>
      
      <div className="quiz-progress">
        <span>问题 {currentQuestionIndex + 1} / {quiz.questions.length}</span>
        <div className="progress-bar">
          <div style={{width: `${((currentQuestionIndex + 1) / quiz.questions.length) * 100}%`}}></div>
        </div>
      </div>
      
      <div className="question-container">
        <QuestionDisplay
          question={currentQuestion}
          answer={responses[currentQuestion?.id]}
          onAnswerChange={handleAnswerChange}
        />
      </div>
      
      <div className="navigation-buttons">
        <button onClick={handlePrevious} disabled={currentQuestionIndex === 0}>
          上一题
        </button>
        
        <button onClick={handleNext} disabled={currentQuestionIndex === quiz.questions.length - 1}>
          下一题
        </button>
        
        <button onClick={handleSubmit} className="submit-btn">
          提交测验
        </button>
      </div>
      
      <div className="question-navigator">
        <h3>题目导航</h3>
        <div className="question-grid">
          {quiz.questions.map((_, idx) => (
            <button
              key={idx}
              className={`question-btn ${
                idx === currentQuestionIndex ? 'current' : ''
              } ${responses[quiz.questions[idx].id] ? 'answered' : ''}`}
              onClick={() => setCurrentQuestionIndex(idx)}
            >
              {idx + 1}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

自动评分

答题评分引擎

class QuizScorer {
  constructor(quiz) {
    this.quiz = quiz;
  }
  
  // 评分所有答案
  scoreQuiz(responses) {
    const scores = this.quiz.questions.map(question => 
      this.scoreQuestion(question, responses[question.id])
    );
    
    const totalScore = scores.reduce((sum, score) => sum + score.points, 0);
    const percentage = (totalScore / this.getMaxPoints()) * 100;
    
    return {
      totalScore,
      maxPoints: this.getMaxPoints(),
      percentage,
      passed: percentage >= this.quiz.passingScore,
      details: scores,
    };
  }
  
  // 评分单个问题
  scoreQuestion(question, response) {
    if (!response) {
      return { questionId: question.id, points: 0, correct: false };
    }
    
    let correct = false;
    
    switch (question.type) {
      case 'multiple_choice':
      case 'true_false':
        correct = response === question.correctAnswer;
        break;
        
      case 'fill_blank':
        correct = this.checkFillBlank(response, question);
        break;
        
      case 'short_answer':
        correct = this.checkShortAnswer(response, question);
        break;
        
      case 'matching':
        correct = this.checkMatching(response, question);
        break;
        
      case 'essay':
        // 论文题需要人工评分
        return {
          questionId: question.id,
          points: null,
          correct: null,
          requiresManualGrading: true,
        };
    }
    
    return {
      questionId: question.id,
      points: correct ? question.points : 0,
      correct,
    };
  }
  
  checkFillBlank(response, question) {
    return question.blanks.some(blank => 
      this.fuzzyMatch(response, blank.answer) ||
      blank.alternatives?.some(alt => this.fuzzyMatch(response, alt))
    );
  }
  
  checkShortAnswer(response, question) {
    return question.acceptableAnswers.some(answer =>
      this.fuzzyMatch(response, answer, question.fuzzyMatching)
    );
  }
  
  checkMatching(response, question) {
    return response.every((r, idx) => r === question.pairs[idx].right);
  }
  
  fuzzyMatch(input, expected, fuzzy = false) {
    const normalize = str => str.toLowerCase().trim();
    
    if (!fuzzy) {
      return normalize(input) === normalize(expected);
    }
    
    // 允许小的拼写错误
    return this.levenshteinDistance(normalize(input), normalize(expected)) <= 1;
  }
  
  levenshteinDistance(a, b) {
    const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(0));
    
    for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
    for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
    
    for (let j = 1; j <= b.length; j++) {
      for (let i = 1; i <= a.length; i++) {
        const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
        matrix[j][i] = Math.min(
          matrix[j][i - 1] + 1,
          matrix[j - 1][i] + 1,
          matrix[j - 1][i - 1] + indicator
        );
      }
    }
    
    return matrix[b.length][a.length];
  }
  
  getMaxPoints() {
    return this.quiz.questions.reduce((sum, q) => sum + q.points, 0);
  }
}

// 使用示例
const scorer = new QuizScorer(quiz);
const result = scorer.scoreQuiz(responses);
console.log(`得分: ${result.totalScore}/${result.maxPoints} (${result.percentage.toFixed(1)}%)`);
console.log(`及格: ${result.passed ? '是' : '否'}`);

最佳实践

应该做的事:

  • 多样化的题目类型
  • 清晰的题目表述
  • 合理的时间限制
  • 及时的反馈
  • 防止作弊措施

不应该做的事:

  • 题目含糊不清
  • 时间限制过短
  • 忽视学生反馈
  • 没有安全措施
  • 过度重视单一形式

检查清单

  • 题目质量高
  • 时间限制合理
  • 评分系统准确
  • 反馈有效
  • 安全措施充分