第一版提交,答题功能OK,题库管理待完善
This commit is contained in:
162
api/controllers/adminController.ts
Normal file
162
api/controllers/adminController.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { SystemConfigModel } from '../models';
|
||||
|
||||
export class AdminController {
|
||||
// 管理员登录
|
||||
static async login(req: Request, res: Response) {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名和密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const isValid = await SystemConfigModel.validateAdminLogin(username, password);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 这里可以生成JWT token,简化处理直接返回成功
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
username,
|
||||
token: 'admin-token' // 简化处理
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('管理员登录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '登录失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取抽题配置
|
||||
static async getQuizConfig(req: Request, res: Response) {
|
||||
try {
|
||||
const config = await SystemConfigModel.getQuizConfig();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: config
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('获取抽题配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取抽题配置失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新抽题配置
|
||||
static async updateQuizConfig(req: Request, res: Response) {
|
||||
try {
|
||||
const { singleRatio, multipleRatio, judgmentRatio, textRatio, totalScore } = req.body;
|
||||
|
||||
const config = {
|
||||
singleRatio,
|
||||
multipleRatio,
|
||||
judgmentRatio,
|
||||
textRatio,
|
||||
totalScore
|
||||
};
|
||||
|
||||
await SystemConfigModel.updateQuizConfig(config);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '抽题配置更新成功'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('更新抽题配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '更新抽题配置失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
static async getStatistics(req: Request, res: Response) {
|
||||
try {
|
||||
const { QuizModel } = await import('../models');
|
||||
const statistics = await QuizModel.getStatistics();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statistics
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取统计数据失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 修改管理员密码
|
||||
static async updatePassword(req: Request, res: Response) {
|
||||
try {
|
||||
const { username, oldPassword, newPassword } = req.body;
|
||||
|
||||
if (!username || !oldPassword || !newPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数不完整'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
const isValid = await SystemConfigModel.validateAdminLogin(username, oldPassword);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '原密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
await SystemConfigModel.updateAdminPassword(username, newPassword);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码修改成功'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('修改密码失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '修改密码失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有配置(管理员用)
|
||||
static async getAllConfigs(req: Request, res: Response) {
|
||||
try {
|
||||
const configs = await SystemConfigModel.getAllConfigs();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: configs
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('获取配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取配置失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
198
api/controllers/adminUserController.ts
Normal file
198
api/controllers/adminUserController.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { UserModel } from '../models/user';
|
||||
import { QuizModel } from '../models/quiz';
|
||||
|
||||
export class AdminUserController {
|
||||
static async getUsers(req: Request, res: Response) {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
|
||||
const result = await UserModel.findAll(limit, (page - 1) * limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.users.map((u) => ({ ...u, password: u.password ?? '' })),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: result.total,
|
||||
pages: Math.ceil(result.total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取用户列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
if (!userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'userId不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const user = await UserModel.findById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await UserModel.delete(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '删除用户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async exportUsers(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await UserModel.findAll(10000, 0);
|
||||
const data = result.users.map((u) => ({
|
||||
ID: u.id,
|
||||
姓名: u.name,
|
||||
手机号: u.phone,
|
||||
密码: u.password ?? '',
|
||||
注册时间: u.createdAt
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '导出用户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async importUsers(req: Request, res: Response) {
|
||||
try {
|
||||
const file = (req as any).file;
|
||||
if (!file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请上传Excel文件'
|
||||
});
|
||||
}
|
||||
|
||||
const XLSX = await import('xlsx');
|
||||
const workbook = XLSX.read(file.buffer, { type: 'buffer' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const rows = XLSX.utils.sheet_to_json(worksheet) as any[];
|
||||
|
||||
const errors: string[] = [];
|
||||
let imported = 0;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
try {
|
||||
const name = row['姓名'] || row['name'];
|
||||
const phone = row['手机号'] || row['phone'];
|
||||
const password = row['密码'] || row['password'] || '';
|
||||
|
||||
if (!name || !phone) {
|
||||
errors.push(`第${i + 2}行:姓名或手机号缺失`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await UserModel.create({ name, phone, password });
|
||||
imported++;
|
||||
} catch (error: any) {
|
||||
if (error.message === '手机号已存在') {
|
||||
errors.push(`第${i + 2}行:手机号重复`);
|
||||
} else {
|
||||
errors.push(`第${i + 2}行:${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
imported,
|
||||
total: rows.length,
|
||||
errors
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '导入用户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserRecords(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
|
||||
const result = await QuizModel.findRecordsByUserId(userId, limit, (page - 1) * limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.records,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: result.total,
|
||||
pages: Math.ceil(result.total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取用户答题记录失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getRecordDetail(req: Request, res: Response) {
|
||||
try {
|
||||
const { recordId } = req.params;
|
||||
|
||||
const record = await QuizModel.findRecordById(recordId);
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '答题记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const answers = await QuizModel.findAnswersByRecordId(recordId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
record,
|
||||
answers
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取答题记录详情失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
174
api/controllers/backupController.ts
Normal file
174
api/controllers/backupController.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { UserModel, QuestionModel, QuizModel } from '../models';
|
||||
|
||||
export class BackupController {
|
||||
// 导出用户数据
|
||||
static async exportUsers(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await UserModel.findAll(10000, 0);
|
||||
const usersData = result.users.map(user => ({
|
||||
ID: user.id,
|
||||
姓名: user.name,
|
||||
手机号: user.phone,
|
||||
创建时间: user.createdAt
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: usersData
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '导出用户数据失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出问题数据
|
||||
static async exportQuestions(req: Request, res: Response) {
|
||||
try {
|
||||
const { type } = req.query;
|
||||
|
||||
const result = await QuestionModel.findAll({
|
||||
type: type as string,
|
||||
limit: 10000,
|
||||
offset: 0
|
||||
});
|
||||
|
||||
const questionsData = result.questions.map(question => ({
|
||||
ID: question.id,
|
||||
题目内容: question.content,
|
||||
题型: question.type,
|
||||
题目类别: question.category,
|
||||
选项: question.options ? question.options.join('|') : '',
|
||||
标准答案: Array.isArray(question.answer) ? question.answer.join(',') : question.answer,
|
||||
分值: question.score,
|
||||
创建时间: question.createdAt
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: questionsData
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '导出问题数据失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出答题记录
|
||||
static async exportRecords(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await QuizModel.findAllRecords(10000, 0);
|
||||
const recordsData = result.records.map((record: any) => ({
|
||||
ID: record.id,
|
||||
用户ID: record.userId,
|
||||
用户名: record.userName,
|
||||
手机号: record.userPhone,
|
||||
总得分: record.totalScore,
|
||||
正确题数: record.correctCount,
|
||||
总题数: record.totalCount,
|
||||
答题时间: record.createdAt
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: recordsData
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '导出答题记录失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出答题答案
|
||||
static async exportAnswers(req: Request, res: Response) {
|
||||
try {
|
||||
// 这里简化处理,实际应该分页获取所有答案
|
||||
const answersData: any[] = [];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: answersData
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '导出答题答案失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 数据恢复
|
||||
static async restoreData(req: Request, res: Response) {
|
||||
try {
|
||||
const { users, questions, records, answers } = req.body;
|
||||
|
||||
// 数据验证
|
||||
if (!users && !questions && !records && !answers) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '没有可恢复的数据'
|
||||
});
|
||||
}
|
||||
|
||||
let restoredCount = {
|
||||
users: 0,
|
||||
questions: 0,
|
||||
records: 0,
|
||||
answers: 0
|
||||
};
|
||||
|
||||
// 恢复用户数据
|
||||
if (users && users.length > 0) {
|
||||
for (const user of users) {
|
||||
try {
|
||||
await UserModel.create({
|
||||
name: user.姓名 || user.name,
|
||||
phone: user.手机号 || user.phone
|
||||
});
|
||||
restoredCount.users++;
|
||||
} catch (error) {
|
||||
console.log('用户已存在,跳过:', user.手机号 || user.phone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复题目数据
|
||||
if (questions && questions.length > 0) {
|
||||
for (const question of questions) {
|
||||
try {
|
||||
await QuestionModel.create({
|
||||
content: question.题目内容 || question.content,
|
||||
type: question.题型 || question.type,
|
||||
category: question.题目类别 || question.category || '通用',
|
||||
options: question.选项 ? (question.选项 as string).split('|') : question.options,
|
||||
answer: question.标准答案 || question.answer,
|
||||
score: question.分值 || question.score
|
||||
});
|
||||
restoredCount.questions++;
|
||||
} catch (error) {
|
||||
console.log('题目创建失败,跳过:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '数据恢复成功',
|
||||
data: restoredCount
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '数据恢复失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
87
api/controllers/examSubjectController.ts
Normal file
87
api/controllers/examSubjectController.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ExamSubjectModel } from '../models/examSubject';
|
||||
|
||||
export class ExamSubjectController {
|
||||
static async getSubjects(req: Request, res: Response) {
|
||||
try {
|
||||
const subjects = await ExamSubjectModel.findAll();
|
||||
res.json({
|
||||
success: true,
|
||||
data: subjects
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取考试科目失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async createSubject(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, totalScore, timeLimitMinutes, typeRatios, categoryRatios } = req.body;
|
||||
|
||||
const subject = await ExamSubjectModel.create({
|
||||
name,
|
||||
totalScore,
|
||||
timeLimitMinutes,
|
||||
typeRatios,
|
||||
categoryRatios
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: subject
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || '新增考试科目失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async updateSubject(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, totalScore, timeLimitMinutes, typeRatios, categoryRatios } = req.body;
|
||||
|
||||
const subject = await ExamSubjectModel.update(id, {
|
||||
name,
|
||||
totalScore,
|
||||
timeLimitMinutes,
|
||||
typeRatios,
|
||||
categoryRatios
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: subject
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error.message || '更新考试科目失败';
|
||||
res.status(message === '科目不存在' ? 404 : 400).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteSubject(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await ExamSubjectModel.delete(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error.message || '删除考试科目失败';
|
||||
res.status(message === '科目不存在' ? 404 : 400).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
144
api/controllers/examTaskController.ts
Normal file
144
api/controllers/examTaskController.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ExamTaskModel } from '../models/examTask';
|
||||
|
||||
export class ExamTaskController {
|
||||
static async getTasks(req: Request, res: Response) {
|
||||
try {
|
||||
const tasks = await ExamTaskModel.findAll();
|
||||
res.json({
|
||||
success: true,
|
||||
data: tasks
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取考试任务失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async createTask(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, subjectId, startAt, endAt, userIds } = req.body;
|
||||
|
||||
if (!name || !subjectId || !startAt || !endAt || !Array.isArray(userIds) || userIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数不完整或用户列表为空'
|
||||
});
|
||||
}
|
||||
|
||||
const task = await ExamTaskModel.create({
|
||||
name,
|
||||
subjectId,
|
||||
startAt,
|
||||
endAt,
|
||||
userIds
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: task
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || '新增考试任务失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async updateTask(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, subjectId, startAt, endAt, userIds } = req.body;
|
||||
|
||||
if (!name || !subjectId || !startAt || !endAt || !Array.isArray(userIds) || userIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数不完整或用户列表为空'
|
||||
});
|
||||
}
|
||||
|
||||
const task = await ExamTaskModel.update(id, {
|
||||
name,
|
||||
subjectId,
|
||||
startAt,
|
||||
endAt,
|
||||
userIds
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: task
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error.message || '更新考试任务失败';
|
||||
res.status(message === '任务不存在' ? 404 : 400).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteTask(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await ExamTaskModel.delete(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error.message || '删除考试任务失败';
|
||||
res.status(message === '任务不存在' ? 404 : 400).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getTaskReport(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const report = await ExamTaskModel.getReport(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: report
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error.message || '获取任务报表失败';
|
||||
res.status(message === '任务不存在' ? 404 : 400).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserTasks(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户ID不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const tasks = await ExamTaskModel.getUserTasks(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: tasks
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取用户任务失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
10
api/controllers/index.ts
Normal file
10
api/controllers/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// 导出所有控制器
|
||||
export { UserController } from './userController';
|
||||
export { QuestionController } from './questionController';
|
||||
export { QuizController } from './quizController';
|
||||
export { AdminController } from './adminController';
|
||||
export { BackupController } from './backupController';
|
||||
export { QuestionCategoryController } from './questionCategoryController';
|
||||
export { ExamSubjectController } from './examSubjectController';
|
||||
export { ExamTaskController } from './examTaskController';
|
||||
export { AdminUserController } from './adminUserController';
|
||||
86
api/controllers/questionCategoryController.ts
Normal file
86
api/controllers/questionCategoryController.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { QuestionCategoryModel } from '../models/questionCategory';
|
||||
|
||||
export class QuestionCategoryController {
|
||||
static async getCategories(req: Request, res: Response) {
|
||||
try {
|
||||
const categories = await QuestionCategoryModel.findAll();
|
||||
res.json({
|
||||
success: true,
|
||||
data: categories
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取题目类别失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async createCategory(req: Request, res: Response) {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
if (!name || typeof name !== 'string') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '类别名称不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const category = await QuestionCategoryModel.create(name);
|
||||
res.json({
|
||||
success: true,
|
||||
data: category
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || '新增题目类别失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async updateCategory(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name || typeof name !== 'string') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '类别名称不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const category = await QuestionCategoryModel.update(id, name);
|
||||
res.json({
|
||||
success: true,
|
||||
data: category
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error.message || '更新题目类别失败';
|
||||
res.status(message === '类别不存在' ? 404 : 400).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteCategory(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await QuestionCategoryModel.delete(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error.message || '删除题目类别失败';
|
||||
res.status(message === '类别不存在' ? 404 : 400).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
324
api/controllers/questionController.ts
Normal file
324
api/controllers/questionController.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { QuestionModel, CreateQuestionData, ExcelQuestionData } from '../models';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
export class QuestionController {
|
||||
// 获取题目列表
|
||||
static async getQuestions(req: Request, res: Response) {
|
||||
try {
|
||||
const { type, category, keyword, startDate, endDate, page = 1, limit = 10 } = req.query;
|
||||
|
||||
const result = await QuestionModel.findAll({
|
||||
type: type as string,
|
||||
category: category as string,
|
||||
keyword: keyword as string,
|
||||
startDate: startDate as string,
|
||||
endDate: endDate as string,
|
||||
limit: parseInt(limit as string),
|
||||
offset: (parseInt(page as string) - 1) * parseInt(limit as string)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.questions,
|
||||
pagination: {
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
total: result.total,
|
||||
pages: Math.ceil(result.total / parseInt(limit as string))
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('获取题目列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取题目列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个题目
|
||||
static async getQuestion(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const question = await QuestionModel.findById(id);
|
||||
if (!question) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '题目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: question
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('获取题目失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取题目失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建题目
|
||||
static async createQuestion(req: Request, res: Response) {
|
||||
try {
|
||||
const { content, type, category, options, answer, score } = req.body;
|
||||
|
||||
const questionData: CreateQuestionData = {
|
||||
content,
|
||||
type,
|
||||
category,
|
||||
options,
|
||||
answer,
|
||||
score
|
||||
};
|
||||
|
||||
// 验证题目数据
|
||||
const errors = QuestionModel.validateQuestionData(questionData);
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败',
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
const question = await QuestionModel.create(questionData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: question
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('创建题目失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '创建题目失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新题目
|
||||
static async updateQuestion(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { content, type, category, options, answer, score } = req.body;
|
||||
|
||||
const updateData: Partial<CreateQuestionData> = {};
|
||||
if (content !== undefined) updateData.content = content;
|
||||
if (type !== undefined) updateData.type = type;
|
||||
if (category !== undefined) updateData.category = category;
|
||||
if (options !== undefined) updateData.options = options;
|
||||
if (answer !== undefined) updateData.answer = answer;
|
||||
if (score !== undefined) updateData.score = score;
|
||||
|
||||
const question = await QuestionModel.update(id, updateData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: question
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('更新题目失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '更新题目失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除题目
|
||||
static async deleteQuestion(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const success = await QuestionModel.delete(id);
|
||||
if (!success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '题目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('删除题目失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '删除题目失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Excel导入题目
|
||||
static async importQuestions(req: Request, res: Response) {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请上传Excel文件'
|
||||
});
|
||||
}
|
||||
|
||||
// 读取Excel文件
|
||||
const workbook = XLSX.read(req.file.buffer, { type: 'buffer' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
|
||||
// 转换为JSON数据
|
||||
const rawData = XLSX.utils.sheet_to_json(worksheet);
|
||||
|
||||
// 转换数据格式
|
||||
const questionsData: ExcelQuestionData[] = rawData.map((row: any) => ({
|
||||
content: row['题目内容'] || row['content'],
|
||||
type: QuestionController.mapQuestionType(row['题型'] || row['type']),
|
||||
category: row['题目类别'] || row['category'] || '通用',
|
||||
answer: row['标准答案'] || row['answer'],
|
||||
score: parseInt(row['分值'] || row['score']) || 0,
|
||||
options: QuestionController.parseOptions(row['选项'] || row['options'])
|
||||
}));
|
||||
|
||||
// 验证数据
|
||||
const validation = QuestionModel.validateExcelData(questionsData);
|
||||
if (!validation.valid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Excel数据格式错误',
|
||||
errors: validation.errors
|
||||
});
|
||||
}
|
||||
|
||||
// 批量创建题目
|
||||
const result = await QuestionModel.createMany(questionsData as any[]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
imported: result.success,
|
||||
total: questionsData.length,
|
||||
errors: result.errors
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Excel导入失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || 'Excel导入失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 映射题型
|
||||
private static mapQuestionType(type: string): string {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'单选': 'single',
|
||||
'多选': 'multiple',
|
||||
'判断': 'judgment',
|
||||
'文字描述': 'text',
|
||||
'single': 'single',
|
||||
'multiple': 'multiple',
|
||||
'judgment': 'judgment',
|
||||
'text': 'text'
|
||||
};
|
||||
|
||||
return typeMap[type] || 'single';
|
||||
}
|
||||
|
||||
// 解析选项
|
||||
private static parseOptions(optionsStr: string): string[] | undefined {
|
||||
if (!optionsStr) return undefined;
|
||||
|
||||
// 支持多种分隔符:|、;、\n
|
||||
const separators = ['|', ';', '\n'];
|
||||
for (const separator of separators) {
|
||||
if (optionsStr.includes(separator)) {
|
||||
return optionsStr.split(separator).map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到分隔符,返回单个选项
|
||||
return [optionsStr.trim()];
|
||||
}
|
||||
|
||||
// Excel导出题目
|
||||
static async exportQuestions(req: Request, res: Response) {
|
||||
try {
|
||||
const { type, category } = req.query;
|
||||
|
||||
// 获取所有题目数据(使用大的limit值获取所有题目)
|
||||
const result = await QuestionModel.findAll({
|
||||
type: type as string,
|
||||
category: category as string,
|
||||
limit: 10000, // 使用大的limit值获取所有题目
|
||||
offset: 0
|
||||
});
|
||||
|
||||
const questions = result.questions;
|
||||
|
||||
// 转换为Excel数据格式
|
||||
const excelData = questions.map((question: any) => ({
|
||||
'题目ID': question.id,
|
||||
'题目内容': question.content,
|
||||
'题型': this.getQuestionTypeLabel(question.type),
|
||||
'题目类别': question.category || '通用',
|
||||
'选项': question.options ? question.options.join('|') : '',
|
||||
'标准答案': question.answer,
|
||||
'分值': question.score,
|
||||
'创建时间': new Date(question.createdAt).toLocaleString()
|
||||
}));
|
||||
|
||||
// 创建Excel工作簿和工作表
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||
|
||||
// 设置列宽
|
||||
const columnWidths = [
|
||||
{ wch: 10 }, // 题目ID
|
||||
{ wch: 60 }, // 题目内容
|
||||
{ wch: 10 }, // 题型
|
||||
{ wch: 15 }, // 题目类别
|
||||
{ wch: 80 }, // 选项
|
||||
{ wch: 20 }, // 标准答案
|
||||
{ wch: 8 }, // 分值
|
||||
{ wch: 20 } // 创建时间
|
||||
];
|
||||
worksheet['!cols'] = columnWidths;
|
||||
|
||||
// 将工作表添加到工作簿
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '题目列表');
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=questions_${new Date().getTime()}.xlsx`);
|
||||
|
||||
// 生成Excel文件并发送
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
|
||||
res.send(excelBuffer);
|
||||
} catch (error: any) {
|
||||
console.error('Excel导出失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || 'Excel导出失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取题型中文标签
|
||||
private static getQuestionTypeLabel(type: string): string {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'single': '单选题',
|
||||
'multiple': '多选题',
|
||||
'judgment': '判断题',
|
||||
'text': '文字题'
|
||||
};
|
||||
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
}
|
||||
273
api/controllers/quizController.ts
Normal file
273
api/controllers/quizController.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { QuestionModel, QuizModel, SystemConfigModel } from '../models';
|
||||
|
||||
export class QuizController {
|
||||
static async generateQuiz(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId, subjectId, taskId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户ID不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
if (taskId) {
|
||||
const { ExamTaskModel } = await import('../models/examTask');
|
||||
const result = await ExamTaskModel.generateQuizQuestions(taskId, userId);
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
questions: result.questions,
|
||||
totalScore: result.totalScore,
|
||||
timeLimit: result.timeLimitMinutes
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subjectId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'subjectId或taskId必须提供其一'
|
||||
});
|
||||
}
|
||||
|
||||
const { ExamSubjectModel } = await import('../models/examSubject');
|
||||
const subject = await ExamSubjectModel.findById(subjectId);
|
||||
if (!subject) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '考试科目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const questions: Awaited<ReturnType<typeof QuestionModel.getRandomQuestions>> = [];
|
||||
|
||||
for (const [type, ratio] of Object.entries(subject.typeRatios)) {
|
||||
if (ratio <= 0) continue;
|
||||
const typeScore = Math.floor((ratio / 100) * subject.totalScore);
|
||||
const avgScore = 10;
|
||||
const count = Math.max(1, Math.round(typeScore / avgScore));
|
||||
|
||||
const categories = Object.entries(subject.categoryRatios)
|
||||
.filter(([, r]) => r > 0)
|
||||
.map(([c]) => c);
|
||||
|
||||
const qs = await QuestionModel.getRandomQuestions(type as any, count, categories);
|
||||
questions.push(...qs);
|
||||
}
|
||||
|
||||
const totalScore = questions.reduce((sum, q) => sum + q.score, 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
questions,
|
||||
totalScore,
|
||||
timeLimit: subject.timeLimitMinutes
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '生成试卷失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async submitQuiz(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId, subjectId, taskId, answers } = req.body;
|
||||
|
||||
if (!userId || !answers || !Array.isArray(answers)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数不完整'
|
||||
});
|
||||
}
|
||||
|
||||
const processedAnswers = [];
|
||||
for (const answer of answers) {
|
||||
const question = await QuestionModel.findById(answer.questionId);
|
||||
if (!question) {
|
||||
processedAnswers.push(answer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (question.type === 'multiple') {
|
||||
const optionCount = question.options ? question.options.length : 0;
|
||||
const unitScore = optionCount > 0 ? question.score / optionCount : 0;
|
||||
let userAnsList: string[] = [];
|
||||
if (Array.isArray(answer.userAnswer)) {
|
||||
userAnsList = answer.userAnswer;
|
||||
} else if (typeof answer.userAnswer === 'string') {
|
||||
try {
|
||||
userAnsList = JSON.parse(answer.userAnswer);
|
||||
} catch (e) {
|
||||
userAnsList = [answer.userAnswer];
|
||||
}
|
||||
}
|
||||
|
||||
let correctAnsList: string[] = [];
|
||||
if (Array.isArray(question.answer)) {
|
||||
correctAnsList = question.answer;
|
||||
} else if (typeof question.answer === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(question.answer);
|
||||
if (Array.isArray(parsed)) correctAnsList = parsed;
|
||||
else correctAnsList = [question.answer];
|
||||
} catch {
|
||||
correctAnsList = [question.answer];
|
||||
}
|
||||
}
|
||||
|
||||
const userSet = new Set(userAnsList);
|
||||
const correctSet = new Set(correctAnsList);
|
||||
let isFullCorrect = true;
|
||||
if (userSet.size !== correctSet.size) {
|
||||
isFullCorrect = false;
|
||||
} else {
|
||||
for (const a of userSet) {
|
||||
if (!correctSet.has(a)) {
|
||||
isFullCorrect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isFullCorrect) {
|
||||
answer.score = question.score;
|
||||
answer.isCorrect = true;
|
||||
} else {
|
||||
let tempScore = 0;
|
||||
for (const uAns of userAnsList) {
|
||||
if (correctSet.has(uAns)) {
|
||||
tempScore += unitScore;
|
||||
} else {
|
||||
tempScore -= unitScore;
|
||||
}
|
||||
}
|
||||
let finalScore = Math.max(0, tempScore);
|
||||
finalScore = Math.round(finalScore * 10) / 10;
|
||||
answer.score = finalScore;
|
||||
answer.isCorrect = false;
|
||||
}
|
||||
} else if (question.type === 'single' || question.type === 'judgment') {
|
||||
const isCorrect = answer.userAnswer === question.answer;
|
||||
answer.score = isCorrect ? question.score : 0;
|
||||
answer.isCorrect = isCorrect;
|
||||
}
|
||||
|
||||
processedAnswers.push(answer);
|
||||
}
|
||||
|
||||
const result = await QuizModel.submitQuiz({ userId, answers: processedAnswers });
|
||||
|
||||
if (subjectId || taskId) {
|
||||
const sql = `
|
||||
UPDATE quiz_records
|
||||
SET subject_id = ?, task_id = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
await import('../database').then(({ run }) => run(sql, [subjectId || null, taskId || null, result.record.id]));
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
recordId: result.record.id,
|
||||
totalScore: result.record.totalScore,
|
||||
correctCount: result.record.correctCount,
|
||||
totalCount: result.record.totalCount
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '提交答题失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserRecords(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
|
||||
const result = await QuizModel.findRecordsByUserId(userId, limit, (page - 1) * limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.records,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: result.total,
|
||||
pages: Math.ceil(result.total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取答题记录失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getRecordDetail(req: Request, res: Response) {
|
||||
try {
|
||||
const { recordId } = req.params;
|
||||
|
||||
const record = await QuizModel.findRecordById(recordId);
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '答题记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const answers = await QuizModel.findAnswersByRecordId(recordId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
record,
|
||||
answers
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取答题记录详情失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getAllRecords(req: Request, res: Response) {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
|
||||
const result = await QuizModel.findAllRecords(limit, (page - 1) * limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.records,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: result.total,
|
||||
pages: Math.ceil(result.total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取答题记录失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
118
api/controllers/userController.ts
Normal file
118
api/controllers/userController.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { UserModel } from '../models/user';
|
||||
|
||||
export class UserController {
|
||||
static async createUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, phone, password } = req.body;
|
||||
|
||||
if (!name || !phone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '姓名和手机号不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const errors = UserModel.validateUserData({ name, phone, password });
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败',
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
const user = await UserModel.create({ name, phone, password });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('创建用户失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '创建用户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const user = await UserModel.findById(id);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取用户信息失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async validateUserInfo(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, phone, password } = req.body;
|
||||
|
||||
if (!name || !phone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '姓名和手机号不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const errors = UserModel.validateUserData({ name, phone, password });
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败',
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
const existingUser = await UserModel.findByPhone(phone);
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.password && existingUser.password !== password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
if (!existingUser.password && password) {
|
||||
await UserModel.updatePasswordById(existingUser.id, password);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: existingUser
|
||||
});
|
||||
} else {
|
||||
const newUser = await UserModel.create({ name, phone, password });
|
||||
res.json({
|
||||
success: true,
|
||||
data: newUser
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('验证用户信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '验证用户信息失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
78
api/controllers/userQuizController.ts
Normal file
78
api/controllers/userQuizController.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ExamTaskModel } from '../models/examTask';
|
||||
|
||||
export class UserQuizController {
|
||||
static async generateQuiz(req: Request, res: Response) {
|
||||
try {
|
||||
const { userId, subjectId, taskId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户ID不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
if (taskId) {
|
||||
const result = await ExamTaskModel.generateQuizQuestions(taskId, userId);
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
questions: result.questions,
|
||||
totalScore: result.totalScore,
|
||||
timeLimit: result.timeLimitMinutes
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subjectId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'subjectId或taskId必须提供其一'
|
||||
});
|
||||
}
|
||||
|
||||
const { QuestionModel, ExamSubjectModel } = await import('../models');
|
||||
const subject = await ExamSubjectModel.findById(subjectId);
|
||||
if (!subject) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '考试科目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const questions: Awaited<ReturnType<typeof QuestionModel.getRandomQuestions>> = [];
|
||||
|
||||
for (const [type, ratio] of Object.entries(subject.typeRatios)) {
|
||||
if (ratio <= 0) continue;
|
||||
const typeScore = Math.floor((ratio / 100) * subject.totalScore);
|
||||
const avgScore = 10;
|
||||
const count = Math.max(1, Math.round(typeScore / avgScore));
|
||||
|
||||
const categories = Object.entries(subject.categoryRatios)
|
||||
.filter(([, r]) => r > 0)
|
||||
.map(([c]) => c);
|
||||
|
||||
const qs = await QuestionModel.getRandomQuestions(type as any, count, categories);
|
||||
questions.push(...qs);
|
||||
}
|
||||
|
||||
const totalScore = questions.reduce((sum, q) => sum + q.score, 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
questions,
|
||||
totalScore,
|
||||
timeLimit: subject.timeLimitMinutes
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '生成试卷失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user