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 页面布局
  • 选项交互设计
  • 答案解析显示

实现每日学习打卡功能!

相关推荐
IT_陈寒1 小时前
Vite的热更新突然失效,原来是因为这个配置
前端·人工智能·后端
ZC跨境爬虫1 小时前
3D 地球卫星轨道可视化平台开发 Day8(分步渲染200颗卫星+ 前端分页控制)
前端·python·3d·重构·html
竹林8181 小时前
RainbowKit快速集成多链钱包连接,我如何从“连不上”到“丝滑切换”
前端·javascript
笨笨狗吞噬者1 小时前
Opus 4.7 使用体验
前端·ai编程
No8g攻城狮2 小时前
【前端】Vue 中 const、var、let 的区别
前端·javascript·vue.js
文心快码BaiduComate2 小时前
Comate搭载Kimi K2.6,长程13h!
前端·后端·程序员
peterfei2 小时前
若爱 IfAI v0.4.2 发布:技能市场上线,重新定义 AI 编辑器的可扩展性
人工智能·开源
Hommy882 小时前
【开源剪映小助手】视频生成流程
开源·github·音视频·剪映小助手
豹哥学前端2 小时前
新手小白学前端day4: Position定位
前端
fishmemory7sec2 小时前
Vue大屏自适应容器组件:v-scale-screen
前端·javascript·vue.js