chore: initial import to Nexus_Edu

This commit is contained in:
SpecialX
2025-11-28 19:23:19 +08:00
commit 38244630a7
153 changed files with 22541 additions and 0 deletions

View 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' });
}
};