Files
Web_BLV_OA_Exam_Prod/api/models/examTask.ts

260 lines
7.9 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { get, query, run, all } from '../database';
export interface ExamTask {
id: string;
name: string;
subjectId: string;
startAt: string;
endAt: string;
createdAt: string;
}
export interface ExamTaskUser {
id: string;
taskId: string;
userId: string;
createdAt: string;
}
export interface TaskWithSubject extends ExamTask {
subjectName: string;
userCount: number;
}
export interface TaskReport {
taskId: string;
taskName: string;
subjectName: string;
totalUsers: number;
completedUsers: number;
averageScore: number;
topScore: number;
lowestScore: number;
details: Array<{
userId: string;
userName: string;
userPhone: string;
score: number | null;
completedAt: string | null;
}>;
}
export class ExamTaskModel {
static async findAll(): Promise<TaskWithSubject[]> {
const sql = `
SELECT
t.id,
t.name,
t.subject_id as subjectId,
t.start_at as startAt,
t.end_at as endAt,
t.created_at as createdAt,
s.name as subjectName,
COUNT(DISTINCT etu.user_id) as userCount
FROM exam_tasks t
JOIN exam_subjects s ON t.subject_id = s.id
LEFT JOIN exam_task_users etu ON t.id = etu.task_id
GROUP BY t.id
ORDER BY t.created_at DESC
`;
return query(sql);
}
static async findById(id: string): Promise<ExamTask | null> {
const sql = `SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt FROM exam_tasks WHERE id = ?`;
const row = await get(sql, [id]);
return row || null;
}
static async create(data: {
name: string;
subjectId: string;
startAt: string;
endAt: string;
userIds: string[];
}): Promise<ExamTask> {
if (!data.name.trim()) throw new Error('任务名称不能为空');
if (!data.userIds.length) throw new Error('至少选择一位用户');
const subject = await import('./examSubject').then(({ ExamSubjectModel }) => ExamSubjectModel.findById(data.subjectId));
if (!subject) throw new Error('科目不存在');
const id = uuidv4();
const sqlTask = `
INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at)
VALUES (?, ?, ?, ?, ?)
`;
const sqlTaskUser = `
INSERT INTO exam_task_users (id, task_id, user_id)
VALUES (?, ?, ?)
`;
await run(sqlTask, [id, data.name.trim(), data.subjectId, data.startAt, data.endAt]);
for (const userId of data.userIds) {
await run(sqlTaskUser, [uuidv4(), id, userId]);
}
return (await this.findById(id)) as ExamTask;
}
static async update(id: string, data: {
name: string;
subjectId: string;
startAt: string;
endAt: string;
userIds: string[];
}): Promise<ExamTask> {
const existing = await this.findById(id);
if (!existing) throw new Error('任务不存在');
if (!data.name.trim()) throw new Error('任务名称不能为空');
if (!data.userIds.length) throw new Error('至少选择一位用户');
const subject = await import('./examSubject').then(({ ExamSubjectModel }) => ExamSubjectModel.findById(data.subjectId));
if (!subject) throw new Error('科目不存在');
await run(`UPDATE exam_tasks SET name = ?, subject_id = ?, start_at = ?, end_at = ? WHERE id = ?`, [
data.name.trim(),
data.subjectId,
data.startAt,
data.endAt,
id
]);
await run(`DELETE FROM exam_task_users WHERE task_id = ?`, [id]);
for (const userId of data.userIds) {
await run(`INSERT INTO exam_task_users (id, task_id, user_id) VALUES (?, ?, ?)`, [uuidv4(), id, userId]);
}
return (await this.findById(id)) as ExamTask;
}
static async delete(id: string): Promise<void> {
const existing = await this.findById(id);
if (!existing) throw new Error('任务不存在');
await run(`DELETE FROM exam_tasks WHERE id = ?`, [id]);
}
static async getReport(taskId: string): Promise<TaskReport> {
const task = await this.findById(taskId);
if (!task) throw new Error('任务不存在');
const subject = await import('./examSubject').then(({ ExamSubjectModel }) => ExamSubjectModel.findById(task.subjectId));
if (!subject) throw new Error('科目不存在');
const sqlUsers = `
SELECT
u.id as userId,
u.name as userName,
u.phone as userPhone,
qr.total_score as score,
qr.created_at as completedAt
FROM exam_task_users etu
JOIN users u ON etu.user_id = u.id
LEFT JOIN quiz_records qr ON u.id = qr.user_id AND qr.task_id = ?
WHERE etu.task_id = ?
`;
const rows = await query(sqlUsers, [taskId, taskId]);
const details = rows.map((r) => ({
userId: r.userId,
userName: r.userName,
userPhone: r.userPhone,
score: r.score !== null ? r.score : null,
completedAt: r.completedAt || null
}));
const completedUsers = details.filter((d) => d.score !== null).length;
const scores = details.map((d) => d.score).filter((s) => s !== null) as number[];
return {
taskId,
taskName: task.name,
subjectName: subject.name,
totalUsers: details.length,
completedUsers,
averageScore: scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0,
topScore: scores.length > 0 ? Math.max(...scores) : 0,
lowestScore: scores.length > 0 ? Math.min(...scores) : 0,
details
};
}
static async generateQuizQuestions(taskId: string, userId: string): Promise<{
questions: Awaited<ReturnType<typeof import('./question').QuestionModel.getRandomQuestions>>;
totalScore: number;
timeLimitMinutes: number;
}> {
const task = await this.findById(taskId);
if (!task) throw new Error('任务不存在');
const now = new Date();
if (now < new Date(task.startAt) || now > new Date(task.endAt)) {
throw new Error('当前时间不在任务有效范围内');
}
const isAssigned = await get(
`SELECT 1 FROM exam_task_users WHERE task_id = ? AND user_id = ?`,
[taskId, userId]
);
if (!isAssigned) throw new Error('用户未被分派到此任务');
const subject = await import('./examSubject').then(({ ExamSubjectModel }) => ExamSubjectModel.findById(task.subjectId));
if (!subject) throw new Error('科目不存在');
const { QuestionModel } = await import('./question');
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);
return {
questions,
totalScore,
timeLimitMinutes: subject.timeLimitMinutes
};
}
static async getUserTasks(userId: string): Promise<ExamTask[]> {
const now = new Date().toISOString();
const rows = await all(`
SELECT t.*, s.name as subjectName, s.totalScore, s.timeLimitMinutes
FROM exam_tasks t
INNER JOIN exam_task_users tu ON t.id = tu.task_id
INNER JOIN exam_subjects s ON t.subject_id = s.id
WHERE tu.user_id = ? AND t.start_at <= ? AND t.end_at >= ?
ORDER BY t.start_at DESC
`, [userId, now, now]);
return rows.map(row => ({
id: row.id,
name: row.name,
subjectId: row.subject_id,
startAt: row.start_at,
endAt: row.end_at,
createdAt: row.created_at,
subjectName: row.subjectName,
totalScore: row.totalScore,
timeLimitMinutes: row.timeLimitMinutes
}));
}
}