Files
Web_BLV_OA_Exam_Prod/api/controllers/adminController.ts
2025-12-23 00:35:57 +08:00

453 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Request, Response } from 'express';
import { SystemConfigModel } from '../models';
import { query } from '../database';
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 || '获取活跃任务统计数据失败'
});
}
}
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 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 || '获取配置失败'
});
}
}
}