142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
|
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||
|
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
export class QuizService {
|
||
|
|
constructor(private readonly prisma: PrismaService) {}
|
||
|
|
|
||
|
|
async create(userId: string, dto: { knowledgeBaseId: string; title?: string; sourceType?: string; sourceId?: string; questionCount?: number }) {
|
||
|
|
const count = dto.questionCount ?? 5;
|
||
|
|
const kb = await this.prisma.knowledgeBase.findUnique({ where: { id: dto.knowledgeBaseId } });
|
||
|
|
if (!kb || kb.deletedAt) throw new NotFoundException('知识库不存在');
|
||
|
|
|
||
|
|
const quiz = await this.prisma.quiz.create({
|
||
|
|
data: {
|
||
|
|
userId, knowledgeBaseId: dto.knowledgeBaseId,
|
||
|
|
title: dto.title || `${kb.title} - 自测`,
|
||
|
|
sourceType: dto.sourceType ?? 'kb', sourceId: dto.sourceId ?? null,
|
||
|
|
questionCount: count,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// Generate questions from KB items
|
||
|
|
const items = await this.prisma.knowledgeItem.findMany({
|
||
|
|
where: { knowledgeBaseId: dto.knowledgeBaseId, deletedAt: null },
|
||
|
|
orderBy: { updatedAt: 'desc' }, take: count * 2,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (items.length === 0) throw new BadRequestException('知识库中没有知识点,无法生成测验');
|
||
|
|
|
||
|
|
const shuffled = items.sort(() => Math.random() - 0.5).slice(0, count);
|
||
|
|
const questions: any[] = [];
|
||
|
|
|
||
|
|
for (let i = 0; i < shuffled.length; i++) {
|
||
|
|
const item = shuffled[i];
|
||
|
|
const otherItems = items.filter(x => x.id !== item.id);
|
||
|
|
|
||
|
|
// Alternate question types
|
||
|
|
const types = ['choice', 'fill', 'judge'];
|
||
|
|
const qType = types[i % 3];
|
||
|
|
|
||
|
|
let stem = '', options: string[] = [], answer = '';
|
||
|
|
|
||
|
|
if (qType === 'choice') {
|
||
|
|
stem = `${item.title} 是什么?`;
|
||
|
|
const correct = item.content?.slice(0, 80) ?? '正确答案';
|
||
|
|
options = [correct];
|
||
|
|
for (const o of otherItems.slice(0, 3)) {
|
||
|
|
options.push(o.content?.slice(0, 80) ?? '其他选项');
|
||
|
|
}
|
||
|
|
options = options.sort(() => Math.random() - 0.5);
|
||
|
|
answer = String(options.indexOf(correct));
|
||
|
|
} else if (qType === 'fill') {
|
||
|
|
const words = (item.content ?? '').split(/[,。;\s]+/).filter(w => w.length >= 3);
|
||
|
|
const blank = words.length > 0 ? words[Math.floor(Math.random() * words.length)] : '关键概念';
|
||
|
|
stem = `${item.title}:请填写缺失的关键词。${(item.content ?? '').replace(blank, '____')}`;
|
||
|
|
answer = blank;
|
||
|
|
} else {
|
||
|
|
const isCorrect = Math.random() > 0.5;
|
||
|
|
stem = `关于「${item.title}」,以下说法是否正确?${isCorrect ? item.content?.slice(0, 100) ?? '正确描述' : '错误描述'}`;
|
||
|
|
answer = String(isCorrect);
|
||
|
|
}
|
||
|
|
|
||
|
|
questions.push({
|
||
|
|
quizId: quiz.id, type: qType, stem,
|
||
|
|
options: options.length > 0 ? options : undefined,
|
||
|
|
answer, explanation: item.content?.slice(0, 200) ?? '',
|
||
|
|
orderIndex: i,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
await this.prisma.quizQuestion.createMany({ data: questions });
|
||
|
|
|
||
|
|
return this.prisma.quiz.findUnique({ where: { id: quiz.id }, include: { questions: { orderBy: { orderIndex: 'asc' } } } });
|
||
|
|
}
|
||
|
|
|
||
|
|
async findAll(userId: string, kbId?: string) {
|
||
|
|
return this.prisma.quiz.findMany({
|
||
|
|
where: { userId, ...(kbId ? { knowledgeBaseId: kbId } : {}) },
|
||
|
|
orderBy: { createdAt: 'desc' },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async findOne(id: string) {
|
||
|
|
const quiz = await this.prisma.quiz.findUnique({
|
||
|
|
where: { id }, include: { questions: { orderBy: { orderIndex: 'asc' } } },
|
||
|
|
});
|
||
|
|
if (!quiz) throw new NotFoundException('测验不存在');
|
||
|
|
return quiz;
|
||
|
|
}
|
||
|
|
|
||
|
|
async start(userId: string, quizId: string) {
|
||
|
|
const quiz = await this.prisma.quiz.findUnique({ where: { id: quizId } });
|
||
|
|
if (!quiz) throw new NotFoundException('测验不存在');
|
||
|
|
|
||
|
|
return this.prisma.quizAttempt.create({
|
||
|
|
data: { quizId, userId, totalQuestions: quiz.questionCount },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async submit(userId: string, attemptId: string, answers: { questionId: string; answer: string }[]) {
|
||
|
|
const attempt = await this.prisma.quizAttempt.findUnique({ where: { id: attemptId } });
|
||
|
|
if (!attempt || attempt.userId !== userId) throw new NotFoundException('答题记录不存在');
|
||
|
|
if (attempt.finishedAt) throw new BadRequestException('已提交过答案');
|
||
|
|
|
||
|
|
let correctCount = 0;
|
||
|
|
|
||
|
|
for (const a of answers) {
|
||
|
|
const q = await this.prisma.quizQuestion.findUnique({ where: { id: a.questionId } });
|
||
|
|
const isCorrect = q?.answer === a.answer;
|
||
|
|
if (isCorrect) correctCount++;
|
||
|
|
await this.prisma.quizAnswer.create({
|
||
|
|
data: { attemptId, questionId: a.questionId, userAnswer: a.answer, isCorrect },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const score = answers.length > 0 ? Math.round((correctCount / answers.length) * 100) : 0;
|
||
|
|
|
||
|
|
await this.prisma.quizAttempt.update({
|
||
|
|
where: { id: attemptId },
|
||
|
|
data: { correctCount, score, finishedAt: new Date() },
|
||
|
|
});
|
||
|
|
|
||
|
|
return { score, correctCount, totalQuestions: answers.length, finishedAt: new Date() };
|
||
|
|
}
|
||
|
|
|
||
|
|
async getResults(attemptId: string) {
|
||
|
|
const attempt = await this.prisma.quizAttempt.findUnique({
|
||
|
|
where: { id: attemptId },
|
||
|
|
include: { answers: { include: { question: true } } },
|
||
|
|
});
|
||
|
|
if (!attempt) throw new NotFoundException('答题记录不存在');
|
||
|
|
return attempt;
|
||
|
|
}
|
||
|
|
|
||
|
|
async getHistory(userId: string) {
|
||
|
|
return this.prisma.quizAttempt.findMany({
|
||
|
|
where: { userId }, orderBy: { startedAt: 'desc' }, take: 50,
|
||
|
|
include: { quiz: { select: { title: true } } },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|