第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 设计
✅ 理解了测验数据结构
✅ 实现了答案验证逻辑
✅ 完成了分数计算功能
✅ 实现了测验历史记录
课后练习
-
添加计时功能:记录每道题的答题时间
-
添加统计分析:统计各题目的正确率
-
添加排行榜:实现测验分数排行
下次预告
第22次:每日一题功能
我们将开发每日一题功能:
- 每日一题算法设计
- QuizPage 页面布局
- 选项交互设计
- 答案解析显示
实现每日学习打卡功能!