chore: initial import to Nexus_Edu
This commit is contained in:
235
backend/src/controllers/question.controller.ts
Normal file
235
backend/src/controllers/question.controller.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import prisma from '../utils/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// POST /api/questions/search
|
||||
// 简单的题目搜索(按科目、难度筛选)
|
||||
export const searchQuestions = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
subjectId,
|
||||
questionType,
|
||||
difficulty, // exact match (legacy)
|
||||
difficultyMin,
|
||||
difficultyMax,
|
||||
keyword,
|
||||
createdBy, // 'me' or specific userId
|
||||
sortBy = 'latest', // 'latest' | 'popular'
|
||||
page = 1,
|
||||
pageSize = 10
|
||||
} = req.body;
|
||||
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: any = {
|
||||
isDeleted: false,
|
||||
...(subjectId && { subjectId }),
|
||||
...(questionType && { questionType }),
|
||||
...(keyword && { content: { contains: keyword } }),
|
||||
};
|
||||
|
||||
// Difficulty range
|
||||
if (difficultyMin || difficultyMax) {
|
||||
where.difficulty = {};
|
||||
if (difficultyMin) where.difficulty.gte = difficultyMin;
|
||||
if (difficultyMax) where.difficulty.lte = difficultyMax;
|
||||
} else if (difficulty) {
|
||||
where.difficulty = difficulty;
|
||||
}
|
||||
|
||||
// CreatedBy filter
|
||||
if (createdBy === 'me') {
|
||||
where.createdBy = req.userId;
|
||||
} else if (createdBy) {
|
||||
where.createdBy = createdBy;
|
||||
}
|
||||
|
||||
// Sorting
|
||||
let orderBy: any = { createdAt: 'desc' };
|
||||
if (sortBy === 'popular') {
|
||||
orderBy = { usageCount: 'desc' }; // Assuming usageCount exists, otherwise fallback to createdAt
|
||||
}
|
||||
|
||||
// 查询题目
|
||||
const [questions, totalCount] = await Promise.all([
|
||||
prisma.question.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
questionType: true,
|
||||
difficulty: true,
|
||||
answer: true,
|
||||
explanation: true,
|
||||
createdAt: true,
|
||||
createdBy: true,
|
||||
knowledgePoints: {
|
||||
select: {
|
||||
knowledgePoint: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
skip,
|
||||
take: pageSize,
|
||||
orderBy
|
||||
}),
|
||||
prisma.question.count({ where })
|
||||
]);
|
||||
|
||||
// 映射到前端 DTO
|
||||
const items = questions.map(q => ({
|
||||
id: q.id,
|
||||
content: q.content,
|
||||
type: q.questionType,
|
||||
difficulty: q.difficulty,
|
||||
answer: q.answer,
|
||||
parse: q.explanation,
|
||||
knowledgePoints: q.knowledgePoints.map(kp => kp.knowledgePoint.name),
|
||||
isMyQuestion: q.createdBy === req.userId
|
||||
}));
|
||||
|
||||
res.json({
|
||||
items,
|
||||
totalCount,
|
||||
pageIndex: page,
|
||||
pageSize
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Search questions error:', error);
|
||||
res.status(500).json({ error: 'Failed to search questions' });
|
||||
}
|
||||
};
|
||||
|
||||
// POST /api/questions
|
||||
// 创建题目
|
||||
export const createQuestion = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { subjectId, content, questionType, difficulty = 3, answer, explanation, optionsConfig, knowledgePoints } = req.body;
|
||||
|
||||
if (!subjectId || !content || !questionType || !answer) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const questionId = uuidv4();
|
||||
|
||||
// Handle knowledge points connection if provided
|
||||
// This is a simplified version, ideally we should resolve KP IDs first
|
||||
|
||||
const question = await prisma.question.create({
|
||||
data: {
|
||||
id: questionId,
|
||||
subjectId,
|
||||
content,
|
||||
questionType,
|
||||
difficulty,
|
||||
answer,
|
||||
explanation,
|
||||
optionsConfig: optionsConfig || null,
|
||||
createdBy: req.userId!,
|
||||
updatedBy: req.userId!
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: question.id,
|
||||
message: 'Question created successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create question error:', error);
|
||||
res.status(500).json({ error: 'Failed to create question' });
|
||||
}
|
||||
};
|
||||
|
||||
// PUT /api/questions/:id
|
||||
export const updateQuestion = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { content, questionType, difficulty, answer, explanation, optionsConfig } = req.body;
|
||||
|
||||
const question = await prisma.question.findUnique({ where: { id } });
|
||||
if (!question) return res.status(404).json({ error: 'Question not found' });
|
||||
|
||||
// Only creator can update (or admin)
|
||||
if (question.createdBy !== req.userId) {
|
||||
// For now, let's assume strict ownership.
|
||||
// In real app, check role.
|
||||
return res.status(403).json({ error: 'Permission denied' });
|
||||
}
|
||||
|
||||
await prisma.question.update({
|
||||
where: { id },
|
||||
data: {
|
||||
content,
|
||||
questionType,
|
||||
difficulty,
|
||||
answer,
|
||||
explanation,
|
||||
optionsConfig: optionsConfig || null,
|
||||
updatedBy: req.userId!
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ message: 'Question updated successfully' });
|
||||
} catch (error) {
|
||||
console.error('Update question error:', error);
|
||||
res.status(500).json({ error: 'Failed to update question' });
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /api/questions/:id
|
||||
export const deleteQuestion = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const question = await prisma.question.findUnique({ where: { id } });
|
||||
if (!question) return res.status(404).json({ error: 'Question not found' });
|
||||
|
||||
if (question.createdBy !== req.userId) {
|
||||
return res.status(403).json({ error: 'Permission denied' });
|
||||
}
|
||||
|
||||
await prisma.question.update({
|
||||
where: { id },
|
||||
data: { isDeleted: true }
|
||||
});
|
||||
|
||||
res.json({ message: 'Question deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete question error:', error);
|
||||
res.status(500).json({ error: 'Failed to delete question' });
|
||||
}
|
||||
};
|
||||
|
||||
// POST /api/questions/parse-text
|
||||
export const parseText = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { text } = req.body;
|
||||
if (!text) return res.status(400).json({ error: 'Text is required' });
|
||||
|
||||
// 简单的模拟解析逻辑
|
||||
// 假设每行是一个题目,或者用空行分隔
|
||||
const questions = text.split(/\n\s*\n/).map((block: string) => {
|
||||
const lines = block.trim().split('\n');
|
||||
const content = lines[0];
|
||||
const options = lines.slice(1).filter((l: string) => /^[A-D]\./.test(l));
|
||||
|
||||
return {
|
||||
content: content,
|
||||
type: options.length > 0 ? 'SingleChoice' : 'Subjective',
|
||||
options: options.length > 0 ? options : undefined,
|
||||
answer: 'A', // 默认答案
|
||||
parse: '解析暂无'
|
||||
};
|
||||
});
|
||||
|
||||
res.json(questions);
|
||||
} catch (error) {
|
||||
console.error('Parse text error:', error);
|
||||
res.status(500).json({ error: 'Failed to parse text' });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user