Files
Web_BLV_OA_Exam_Prod/api/controllers/adminController.ts

502 lines
14 KiB
TypeScript
Raw Normal View History

import { Request, Response } from 'express';
import { SystemConfigModel } from '../models';
import { query } from '../database';
2025-12-23 00:35:57 +08:00
const toPositiveInt = (value: unknown, defaultValue: number) => {
const n = Number(value);
if (!Number.isFinite(n) || n <= 0) return defaultValue;
return Math.floor(n);
};
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: '用户名和密码不能为空'
});
}
// 直接验证用户名和密码,不依赖数据库
// 初始管理员账号admin / admin123
const isValid = username === 'admin' && password === 'admin123';
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 getActiveTasksStats(req: Request, res: Response) {
try {
const { ExamTaskModel } = await import('../models/examTask');
const stats = await ExamTaskModel.getActiveTasksWithStats();
res.json({
success: true,
data: stats
});
} catch (error: any) {
console.error('获取活跃任务统计数据失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取活跃任务统计数据失败'
});
}
}
2025-12-23 00:35:57 +08:00
static async getHistoryTaskStats(req: Request, res: Response) {
try {
const page = toPositiveInt(req.query.page, 1);
const limit = toPositiveInt(req.query.limit, 5);
const { ExamTaskModel } = await import('../models/examTask');
const result = await ExamTaskModel.getHistoryTasksWithStatsPaged(page, limit);
res.json({
success: true,
data: result.data,
pagination: {
page,
limit,
total: result.total,
pages: Math.ceil(result.total / limit),
},
});
} catch (error: any) {
console.error('获取历史任务统计失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取历史任务统计失败',
});
}
}
static async getUpcomingTaskStats(req: Request, res: Response) {
try {
const page = toPositiveInt(req.query.page, 1);
const limit = toPositiveInt(req.query.limit, 5);
const { ExamTaskModel } = await import('../models/examTask');
const result = await ExamTaskModel.getUpcomingTasksWithStatsPaged(page, limit);
res.json({
success: true,
data: result.data,
pagination: {
page,
limit,
total: result.total,
pages: Math.ceil(result.total / limit),
},
});
} catch (error: any) {
console.error('获取未开始任务统计失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取未开始任务统计失败',
});
}
}
static async getAllTaskStats(req: Request, res: Response) {
try {
const page = toPositiveInt(req.query.page, 1);
const limit = toPositiveInt(req.query.limit, 5);
const statusRaw = typeof req.query.status === 'string' ? req.query.status : undefined;
const status =
statusRaw === 'completed' || statusRaw === 'ongoing' || statusRaw === 'notStarted'
? statusRaw
: undefined;
const endAtStart = typeof req.query.endAtStart === 'string' ? req.query.endAtStart : undefined;
const endAtEnd = typeof req.query.endAtEnd === 'string' ? req.query.endAtEnd : undefined;
if (endAtStart && !Number.isFinite(Date.parse(endAtStart))) {
return res.status(400).json({ success: false, message: 'endAtStart 参数无效' });
}
if (endAtEnd && !Number.isFinite(Date.parse(endAtEnd))) {
return res.status(400).json({ success: false, message: 'endAtEnd 参数无效' });
}
const { ExamTaskModel } = await import('../models/examTask');
const result = await ExamTaskModel.getAllTasksWithStatsPaged({
page,
limit,
status,
endAtStart,
endAtEnd,
});
res.json({
success: true,
data: result.data,
pagination: {
page,
limit,
total: result.total,
pages: Math.ceil(result.total / limit),
},
});
} catch (error: any) {
console.error('获取任务统计失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取任务统计失败',
});
}
}
2025-12-23 00:35:57 +08:00
static async getDashboardOverview(req: Request, res: Response) {
try {
const { QuizModel } = await import('../models');
const statistics = await QuizModel.getStatistics();
const now = new Date().toISOString();
const [categoryRows, activeSubjectRow, statusRow] = await Promise.all([
query(
`
SELECT
COALESCE(category, '未分类') as category,
COUNT(*) as count
FROM questions
GROUP BY COALESCE(category, '未分类')
ORDER BY count DESC
`,
),
query(
`
SELECT COUNT(DISTINCT subject_id) as total
FROM exam_tasks
WHERE start_at <= ? AND end_at >= ?
`,
[now, now],
).then((rows: any[]) => rows[0]),
query(
`
SELECT
SUM(CASE WHEN end_at < ? THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN start_at <= ? AND end_at >= ? THEN 1 ELSE 0 END) as ongoing,
SUM(CASE WHEN start_at > ? THEN 1 ELSE 0 END) as notStarted
FROM exam_tasks
`,
[now, now, now, now],
).then((rows: any[]) => rows[0]),
]);
const questionCategoryStats = (categoryRows as any[]).map((r) => ({
category: r.category,
count: Number(r.count) || 0,
}));
res.json({
success: true,
data: {
totalUsers: statistics.totalUsers,
activeSubjectCount: Number(activeSubjectRow?.total) || 0,
questionCategoryStats,
taskStatusDistribution: {
completed: Number(statusRow?.completed) || 0,
ongoing: Number(statusRow?.ongoing) || 0,
notStarted: Number(statusRow?.notStarted) || 0,
},
},
});
} catch (error: any) {
console.error('获取仪表盘概览失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取仪表盘概览失败',
});
}
}
static async getUserStats(req: Request, res: Response) {
try {
const rows = await query(
`
SELECT
u.id as userId,
u.name as userName,
COUNT(qr.id) as totalRecords,
COALESCE(ROUND(AVG(qr.total_score), 2), 0) as averageScore,
COALESCE(MAX(qr.total_score), 0) as highestScore,
COALESCE(MIN(qr.total_score), 0) as lowestScore
FROM users u
LEFT JOIN quiz_records qr ON u.id = qr.user_id
GROUP BY u.id
ORDER BY totalRecords DESC, u.created_at DESC
`,
);
const data = (rows as any[]).map((row) => ({
userId: row.userId,
userName: row.userName,
totalRecords: Number(row.totalRecords) || 0,
averageScore: Number(row.averageScore) || 0,
highestScore: Number(row.highestScore) || 0,
lowestScore: Number(row.lowestScore) || 0,
}));
res.json({
success: true,
data,
});
} catch (error: any) {
console.error('获取用户统计失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取用户统计失败',
});
}
}
static async getSubjectStats(req: Request, res: Response) {
try {
const rows = await query(
`
SELECT
s.id as subjectId,
s.name as subjectName,
COUNT(qr.id) as totalRecords,
COALESCE(ROUND(AVG(qr.total_score), 2), 0) as averageScore,
COALESCE(ROUND(AVG(
CASE
WHEN qr.total_count > 0 THEN (qr.correct_count * 100.0 / qr.total_count)
ELSE 0
END
), 2), 0) as averageCorrectRate
FROM exam_subjects s
LEFT JOIN quiz_records qr ON s.id = qr.subject_id
GROUP BY s.id
ORDER BY totalRecords DESC, s.created_at DESC
`,
);
const data = (rows as any[]).map((row) => ({
subjectId: row.subjectId,
subjectName: row.subjectName,
totalRecords: Number(row.totalRecords) || 0,
averageScore: Number(row.averageScore) || 0,
averageCorrectRate: Number(row.averageCorrectRate) || 0,
}));
res.json({
success: true,
data,
});
} catch (error: any) {
console.error('获取科目统计失败:', error);
res.status(500).json({
success: false,
message: error.message || '获取科目统计失败',
});
}
}
static async getTaskStats(req: Request, res: Response) {
try {
const rows = await query(
`
SELECT
t.id as taskId,
t.name as taskName,
COALESCE(rs.totalRecords, 0) as totalRecords,
COALESCE(rs.averageScore, 0) as averageScore,
COALESCE(us.totalUsers, 0) as totalUsers,
COALESCE(rs.completedUsers, 0) as completedUsers
FROM exam_tasks t
LEFT JOIN (
SELECT
task_id,
COUNT(*) as totalRecords,
ROUND(AVG(total_score), 2) as averageScore,
COUNT(DISTINCT user_id) as completedUsers
FROM quiz_records
WHERE task_id IS NOT NULL
GROUP BY task_id
) rs ON rs.task_id = t.id
LEFT JOIN (
SELECT
task_id,
COUNT(DISTINCT user_id) as totalUsers
FROM exam_task_users
GROUP BY task_id
) us ON us.task_id = t.id
ORDER BY t.created_at DESC
`,
);
const data = rows.map((row: any) => {
const totalUsers = Number(row.totalUsers) || 0;
const completedUsers = Number(row.completedUsers) || 0;
const completionRate = totalUsers > 0 ? (completedUsers / totalUsers) * 100 : 0;
return {
taskId: row.taskId,
taskName: row.taskName,
totalRecords: Number(row.totalRecords) || 0,
averageScore: Number(row.averageScore) || 0,
completionRate,
};
});
res.json({
success: true,
data,
});
} 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 || '获取配置失败'
});
}
}
}