HarmonyOS APP<玩转React>开源教程二十一:测验服务层实现

第21次:测验服务层实现

测验功能是学习类应用的重要组成部分,帮助用户检验学习成果。本次课程将开发 QuizService,实现测验数据管理、答案验证和分数计算。


学习目标

  • 掌握 QuizService 设计
  • 理解测验数据结构
  • 实现答案验证逻辑
  • 完成分数计算功能
  • 实现测验历史记录

21.1 测验数据结构

Quiz 测验结构

typescript 复制代码
interface Quiz {
  id: string;              // 测验 ID
  moduleId: string;        // 所属模块
  lessonId: string;        // 所属课程
  title: string;           // 测验标题
  passingScore: number;    // 及格分数
  questions: QuizItem[];   // 题目列表
}

QuizItem 题目结构

typescript 复制代码
interface QuizItem {
  id: string;              // 题目 ID
  question: string;        // 题目内容
  options: string[];       // 选项列表
  correctAnswer: number;   // 正确答案索引
  explanation: string;     // 答案解析
}

QuizResult 结果结构

typescript 复制代码
interface QuizResult {
  quizId: string;          // 测验 ID
  score: number;           // 得分(0-100)
  totalQuestions: number;  // 总题数
  passed: boolean;         // 是否通过
  answers: number[];       // 用户答案
  completedAt: string;     // 完成时间
  timeSpent: number;       // 用时(秒)
}

21.2 QuizService 设计

服务结构

typescript 复制代码
export class QuizService {
  // 测验数据缓存
  private static quizzes: Map<string, Quiz> = new Map();
  
  // 测验历史记录
  private static quizHistory: QuizResult[] = [];

  // 初始化
  static init(quizzes: Quiz[]): void;
  
  // 获取测验
  static getQuiz(quizId: string): Quiz | undefined;
  
  // 验证答案
  static validateAnswer(question: QuizItem, userAnswer: number): boolean;
  
  // 计算分数
  static calculateScore(quiz: Quiz, answers: number[]): number;
  
  // 提交测验
  static async submitQuiz(quizId: string, answers: number[], timeSpent: number): Promise<QuizResult>;
}

初始化测验数据

typescript 复制代码
static init(quizzes: Quiz[]): void {
  QuizService.quizzes.clear();
  for (const quiz of quizzes) {
    QuizService.quizzes.set(quiz.id, quiz);
  }
  console.info('[QuizService] Initialized with', quizzes.length, 'quizzes');
}

21.3 答案验证逻辑

验证单个答案

typescript 复制代码
static validateAnswer(question: QuizItem, userAnswer: number): boolean {
  return question.correctAnswer === userAnswer;
}

获取错误答案索引

typescript 复制代码
static getIncorrectAnswers(quiz: Quiz, answers: number[]): number[] {
  const incorrect: number[] = [];

  for (let i = 0; i < quiz.questions.length; i++) {
    // 未作答或答错
    if (i >= answers.length || 
        !QuizService.validateAnswer(quiz.questions[i], answers[i])) {
      incorrect.push(i);
    }
  }

  return incorrect;
}

21.4 分数计算

计算测验分数

typescript 复制代码
static calculateScore(quiz: Quiz, answers: number[]): number {
  if (quiz.questions.length === 0) {
    return 0;
  }

  let correctCount = 0;
  for (let i = 0; i < quiz.questions.length; i++) {
    if (i < answers.length && 
        QuizService.validateAnswer(quiz.questions[i], answers[i])) {
      correctCount++;
    }
  }

  // 返回百分制分数
  return Math.round((correctCount / quiz.questions.length) * 100);
}

判断是否通过

typescript 复制代码
const passed = score >= (quiz.passingScore ?? AppConstants.QUIZ_PASSING_SCORE);

21.5 测验提交与历史

提交测验

typescript 复制代码
static async submitQuiz(
  quizId: string, 
  answers: number[], 
  timeSpent: number
): Promise<QuizResult> {
  const quiz = QuizService.quizzes.get(quizId);
  if (!quiz) {
    throw new Error(`Quiz not found: ${quizId}`);
  }

  // 计算分数
  const score = QuizService.calculateScore(quiz, answers);
  const passed = score >= (quiz.passingScore ?? AppConstants.QUIZ_PASSING_SCORE);

  // 创建结果对象
  const result: QuizResult = {
    quizId,
    score,
    totalQuestions: quiz.questions.length,
    passed,
    answers,
    completedAt: new Date().toISOString(),
    timeSpent
  };

  // 保存到历史记录
  QuizService.quizHistory.push(result);
  await QuizService.saveQuizHistory();

  return result;
}

加载历史记录

typescript 复制代码
static async loadQuizHistory(): Promise<QuizResult[]> {
  try {
    const history = await StorageUtil.getObject<QuizResult[]>(
      StorageKeys.QUIZ_HISTORY,
      []
    );
    QuizService.quizHistory = history;
    return history;
  } catch (error) {
    console.error('[QuizService] Failed to load quiz history:', error);
    return [];
  }
}

保存历史记录

typescript 复制代码
private static async saveQuizHistory(): Promise<void> {
  try {
    await StorageUtil.setObject(
      StorageKeys.QUIZ_HISTORY, 
      QuizService.quizHistory
    );
  } catch (error) {
    console.error('[QuizService] Failed to save quiz history:', error);
  }
}

21.6 查询方法

获取课程测验

typescript 复制代码
static getQuizForLesson(lessonId: string): Quiz | undefined {
  for (const quiz of QuizService.quizzes.values()) {
    if (quiz.lessonId === lessonId) {
      return quiz;
    }
  }
  return undefined;
}

获取最佳成绩

typescript 复制代码
static getBestScore(quizId: string): number {
  const results = QuizService.quizHistory.filter(r => r.quizId === quizId);
  if (results.length === 0) {
    return 0;
  }
  return Math.max(...results.map(r => r.score));
}

检查是否通过

typescript 复制代码
static hasPassedQuiz(quizId: string): boolean {
  return QuizService.quizHistory.some(r => r.quizId === quizId && r.passed);
}

21.7 完整服务代码

typescript 复制代码
/**
 * 测验服务
 */
import { StorageUtil } from '../common/StorageUtil';
import { StorageKeys, AppConstants } from '../common/Constants';
import { Quiz, QuizItem, QuizResult } from '../models/Models';

export class QuizService {
  private static quizzes: Map<string, Quiz> = new Map();
  private static quizHistory: QuizResult[] = [];

  static init(quizzes: Quiz[]): void {
    QuizService.quizzes.clear();
    for (const quiz of quizzes) {
      QuizService.quizzes.set(quiz.id, quiz);
    }
  }

  static async loadQuizHistory(): Promise<QuizResult[]> {
    try {
      const history = await StorageUtil.getObject<QuizResult[]>(
        StorageKeys.QUIZ_HISTORY,
        []
      );
      QuizService.quizHistory = history;
      return history;
    } catch (error) {
      console.error('[QuizService] Failed to load quiz history:', error);
      return [];
    }
  }

  private static async saveQuizHistory(): Promise<void> {
    try {
      await StorageUtil.setObject(StorageKeys.QUIZ_HISTORY, QuizService.quizHistory);
    } catch (error) {
      console.error('[QuizService] Failed to save quiz history:', error);
    }
  }

  static getQuizForLesson(lessonId: string): Quiz | undefined {
    for (const quiz of QuizService.quizzes.values()) {
      if (quiz.lessonId === lessonId) {
        return quiz;
      }
    }
    return undefined;
  }

  static getQuiz(quizId: string): Quiz | undefined {
    return QuizService.quizzes.get(quizId);
  }

  static validateAnswer(question: QuizItem, userAnswer: number): boolean {
    return question.correctAnswer === userAnswer;
  }

  static calculateScore(quiz: Quiz, answers: number[]): number {
    if (quiz.questions.length === 0) return 0;
    let correctCount = 0;
    for (let i = 0; i < quiz.questions.length; i++) {
      if (i < answers.length && QuizService.validateAnswer(quiz.questions[i], answers[i])) {
        correctCount++;
      }
    }
    return Math.round((correctCount / quiz.questions.length) * 100);
  }

  static getIncorrectAnswers(quiz: Quiz, answers: number[]): number[] {
    const incorrect: number[] = [];
    for (let i = 0; i < quiz.questions.length; i++) {
      if (i >= answers.length || !QuizService.validateAnswer(quiz.questions[i], answers[i])) {
        incorrect.push(i);
      }
    }
    return incorrect;
  }

  static async submitQuiz(quizId: string, answers: number[], timeSpent: number): Promise<QuizResult> {
    const quiz = QuizService.quizzes.get(quizId);
    if (!quiz) throw new Error(`Quiz not found: ${quizId}`);

    const score = QuizService.calculateScore(quiz, answers);
    const passed = score >= (quiz.passingScore ?? AppConstants.QUIZ_PASSING_SCORE);

    const result: QuizResult = {
      quizId, score, totalQuestions: quiz.questions.length,
      passed, answers, completedAt: new Date().toISOString(), timeSpent
    };

    QuizService.quizHistory.push(result);
    await QuizService.saveQuizHistory();
    return result;
  }

  static getQuizHistory(): QuizResult[] {
    return QuizService.quizHistory;
  }

  static getBestScore(quizId: string): number {
    const results = QuizService.quizHistory.filter(r => r.quizId === quizId);
    if (results.length === 0) return 0;
    return Math.max(...results.map(r => r.score));
  }

  static hasPassedQuiz(quizId: string): boolean {
    return QuizService.quizHistory.some(r => r.quizId === quizId && r.passed);
  }

  static async clearHistory(): Promise<void> {
    QuizService.quizHistory = [];
    await QuizService.saveQuizHistory();
  }
}

本次课程小结

通过本次课程,你已经:

✅ 掌握了 QuizService 设计

✅ 理解了测验数据结构

✅ 实现了答案验证逻辑

✅ 完成了分数计算功能

✅ 实现了测验历史记录


课后练习

  1. 添加计时功能:记录每道题的答题时间

  2. 添加统计分析:统计各题目的正确率

  3. 添加排行榜:实现测验分数排行


下次预告

第22次:每日一题功能

我们将开发每日一题功能:

  • 每日一题算法设计
  • QuizPage 页面布局
  • 选项交互设计
  • 答案解析显示

实现每日学习打卡功能!

相关推荐
apcipot_rain3 小时前
事无巨细地解释一个vue前端网页
前端·javascript·vue.js
han_3 小时前
JavaScript设计模式(三):代理模式实现与应用
前端·javascript·设计模式
qq_283720053 小时前
Qt QML 中为 ComBox设置鸿蒙字体(HarmonyOS Sans)——适配 Qt 5.6.x 与 Qt 5.12+
c++·qt·harmonyos
~欲买桂花同载酒~3 小时前
项目安装- React + TypeScript
前端·react.js·typescript
光辉GuangHui3 小时前
SDD 实践:OpenSpec + Superpowers 整合创建自定义工作流
前端·后端
ssshooter3 小时前
infer,TS 类型系统的手术刀
前端·面试·typescript
用户3167361303423 小时前
图片懒加载,我总结了三个方式
前端
灰太狼大大王3 小时前
2026 前端基石:HTML5 全景知识体系指南(从入门到架构师思维)
前端
米丘3 小时前
vue-router 5.x 文件式路由
前端·vue.js