2025-12-18 19:07:21 +08:00
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 ;
2025-12-19 16:02:38 +08:00
selectionConfig? : string ; // JSON string
2025-12-18 19:07:21 +08:00
}
2025-12-25 21:54:52 +08:00
export interface UserExamTask extends ExamTask {
subjectName : string ;
totalScore : number ;
timeLimitMinutes : number ;
usedAttempts : number ;
maxAttempts : number ;
bestScore : number ;
}
2025-12-18 19:07:21 +08:00
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 ;
} > ;
}
2025-12-19 00:58:58 +08:00
export interface ActiveTaskStat {
taskId : string ;
taskName : string ;
subjectName : string ;
totalUsers : number ;
completedUsers : number ;
completionRate : number ;
passRate : number ;
excellentRate : number ;
startAt : string ;
endAt : string ;
}
2025-12-18 19:07:21 +08:00
export class ExamTaskModel {
2025-12-23 00:35:57 +08:00
private static buildActiveTaskStat ( input : {
taskId : string ;
taskName : string ;
subjectName : string ;
totalScore : number ;
startAt : string ;
endAt : string ;
report : TaskReport ;
} ) : ActiveTaskStat {
const { report } = input ;
const completionRate =
report . totalUsers > 0
? Math . round ( ( report . completedUsers / report . totalUsers ) * 100 )
: 0 ;
const passingUsers = report . details . filter ( ( d ) = > {
if ( d . score === null ) return false ;
return d . score / input . totalScore >= 0.6 ;
} ) . length ;
const passRate =
report . totalUsers > 0 ? Math . round ( ( passingUsers / report . totalUsers ) * 100 ) : 0 ;
const excellentUsers = report . details . filter ( ( d ) = > {
if ( d . score === null ) return false ;
return d . score / input . totalScore >= 0.8 ;
} ) . length ;
const excellentRate =
report . totalUsers > 0
? Math . round ( ( excellentUsers / report . totalUsers ) * 100 )
: 0 ;
return {
taskId : input.taskId ,
taskName : input.taskName ,
subjectName : input.subjectName ,
totalUsers : report.totalUsers ,
completedUsers : report.completedUsers ,
completionRate ,
passRate ,
excellentRate ,
startAt : input.startAt ,
endAt : input.endAt ,
} ;
}
2025-12-19 00:58:58 +08:00
static async findAll ( ) : Promise < ( TaskWithSubject & {
completedUsers : number ;
passRate : number ;
excellentRate : number ;
} ) [ ] > {
// 1. 先获取所有任务的基本信息
const baseTasks = await all ( `
2025-12-18 19:07:21 +08:00
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 ,
2025-12-19 00:58:58 +08:00
COUNT ( DISTINCT etu . user_id ) as userCount ,
s . total_score as totalScore
2025-12-18 19:07:21 +08:00
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
2025-12-19 00:58:58 +08:00
` );
// 2. 为每个任务计算完成人数、合格率和优秀率
const tasksWithStats : any [ ] = [ ] ;
for ( const task of baseTasks ) {
// 获取该任务的详细报表数据
const report = await this . getReport ( task . id ) ;
// 计算合格率( 得分率60%以上)
const passingUsers = report . details . filter ( ( d : any ) = > {
if ( d . score === null ) return false ;
return ( d . score / task . totalScore ) >= 0.6 ;
} ) . length ;
const passRate = report . totalUsers > 0
? Math . round ( ( passingUsers / report . totalUsers ) * 100 )
: 0 ;
// 计算优秀率( 得分率80%以上)
const excellentUsers = report . details . filter ( ( d : any ) = > {
if ( d . score === null ) return false ;
return ( d . score / task . totalScore ) >= 0.8 ;
} ) . length ;
const excellentRate = report . totalUsers > 0
? Math . round ( ( excellentUsers / report . totalUsers ) * 100 )
: 0 ;
tasksWithStats . push ( {
. . . task ,
completedUsers : report.completedUsers ,
passRate ,
excellentRate
} ) ;
}
return tasksWithStats ;
}
static async getActiveTasksWithStats ( ) : Promise < ActiveTaskStat [ ] > {
const now = new Date ( ) . toISOString ( ) ;
// 1. 获取当前时间有效的任务,包括开始和结束时间
const activeTasks = await all ( `
SELECT
t . id , t . name as taskName , s . name as subjectName , s . total_score as totalScore ,
t . start_at as startAt , t . end_at as endAt
FROM exam_tasks t
JOIN exam_subjects s ON t . subject_id = s . id
WHERE t . start_at <= ? AND t . end_at >= ?
ORDER BY t . created_at DESC
` , [now, now]);
const stats : ActiveTaskStat [ ] = [ ] ;
for ( const task of activeTasks ) {
// 2. 获取每个任务的详细报告数据
const report = await this . getReport ( task . id ) ;
// 3. 计算完成率
const completionRate = report . totalUsers > 0
? Math . round ( ( report . completedUsers / report . totalUsers ) * 100 )
: 0 ;
// 4. 计算合格率( 得分率60%以上)
const passingUsers = report . details . filter ( d = > {
if ( d . score === null ) return false ;
return ( d . score / task . totalScore ) >= 0.6 ;
} ) . length ;
const passRate = report . totalUsers > 0
? Math . round ( ( passingUsers / report . totalUsers ) * 100 )
: 0 ;
// 5. 计算优秀率( 得分率80%以上)
const excellentUsers = report . details . filter ( d = > {
if ( d . score === null ) return false ;
return ( d . score / task . totalScore ) >= 0.8 ;
} ) . length ;
const excellentRate = report . totalUsers > 0
? Math . round ( ( excellentUsers / report . totalUsers ) * 100 )
: 0 ;
stats . push ( {
taskId : task.id ,
taskName : task.taskName ,
subjectName : task.subjectName ,
totalUsers : report.totalUsers ,
completedUsers : report.completedUsers ,
completionRate ,
passRate ,
excellentRate ,
startAt : task.startAt ,
endAt : task.endAt
} ) ;
}
return stats ;
2025-12-18 19:07:21 +08:00
}
2025-12-23 00:35:57 +08:00
static async getHistoryTasksWithStatsPaged (
page : number ,
limit : number ,
) : Promise < { data : ActiveTaskStat [ ] ; total : number } > {
const now = new Date ( ) . toISOString ( ) ;
const offset = ( page - 1 ) * limit ;
const totalRow = await get (
` SELECT COUNT(*) as total FROM exam_tasks t WHERE t.end_at < ? ` ,
[ now ] ,
) ;
const total = Number ( totalRow ? . total || 0 ) ;
const tasks = await all (
`
SELECT
t . id , t . name as taskName , s . name as subjectName , s . total_score as totalScore ,
t . start_at as startAt , t . end_at as endAt
FROM exam_tasks t
JOIN exam_subjects s ON t . subject_id = s . id
WHERE t . end_at < ?
ORDER BY t . end_at DESC
LIMIT ? OFFSET ?
` ,
[ now , limit , offset ] ,
) ;
const data : ActiveTaskStat [ ] = [ ] ;
for ( const task of tasks ) {
const report = await this . getReport ( task . id ) ;
data . push (
this . buildActiveTaskStat ( {
taskId : task.id ,
taskName : task.taskName ,
subjectName : task.subjectName ,
totalScore : Number ( task . totalScore ) || 0 ,
startAt : task.startAt ,
endAt : task.endAt ,
report ,
} ) ,
) ;
}
return { data , total } ;
}
static async getUpcomingTasksWithStatsPaged (
page : number ,
limit : number ,
) : Promise < { data : ActiveTaskStat [ ] ; total : number } > {
const now = new Date ( ) . toISOString ( ) ;
const offset = ( page - 1 ) * limit ;
const totalRow = await get (
` SELECT COUNT(*) as total FROM exam_tasks t WHERE t.start_at > ? ` ,
[ now ] ,
) ;
const total = Number ( totalRow ? . total || 0 ) ;
const tasks = await all (
`
SELECT
t . id , t . name as taskName , s . name as subjectName , s . total_score as totalScore ,
t . start_at as startAt , t . end_at as endAt
FROM exam_tasks t
JOIN exam_subjects s ON t . subject_id = s . id
WHERE t . start_at > ?
ORDER BY t . start_at ASC
LIMIT ? OFFSET ?
` ,
[ now , limit , offset ] ,
) ;
const data : ActiveTaskStat [ ] = [ ] ;
for ( const task of tasks ) {
const report = await this . getReport ( task . id ) ;
data . push (
this . buildActiveTaskStat ( {
taskId : task.id ,
taskName : task.taskName ,
subjectName : task.subjectName ,
totalScore : Number ( task . totalScore ) || 0 ,
startAt : task.startAt ,
endAt : task.endAt ,
report ,
} ) ,
) ;
}
return { data , total } ;
}
2025-12-25 00:15:14 +08:00
static async getAllTasksWithStatsPaged (
input : {
page : number ;
limit : number ;
status ? : 'completed' | 'ongoing' | 'notStarted' ;
endAtStart? : string ;
endAtEnd? : string ;
} ,
) : Promise < { data : Array < ActiveTaskStat & { status : '已完成' | '进行中' | '未开始' } > ; total : number } > {
const nowIso = new Date ( ) . toISOString ( ) ;
const nowMs = Date . now ( ) ;
const offset = ( input . page - 1 ) * input . limit ;
const whereParts : string [ ] = [ ] ;
const params : any [ ] = [ ] ;
if ( input . status === 'completed' ) {
whereParts . push ( 't.end_at < ?' ) ;
params . push ( nowIso ) ;
} else if ( input . status === 'ongoing' ) {
whereParts . push ( 't.start_at <= ? AND t.end_at >= ?' ) ;
params . push ( nowIso , nowIso ) ;
} else if ( input . status === 'notStarted' ) {
whereParts . push ( 't.start_at > ?' ) ;
params . push ( nowIso ) ;
}
if ( input . endAtStart ) {
whereParts . push ( 't.end_at >= ?' ) ;
params . push ( input . endAtStart ) ;
}
if ( input . endAtEnd ) {
whereParts . push ( 't.end_at <= ?' ) ;
params . push ( input . endAtEnd ) ;
}
const whereClause = whereParts . length > 0 ? ` WHERE ${ whereParts . join ( ' AND ' ) } ` : '' ;
const totalRow = await get ( ` SELECT COUNT(*) as total FROM exam_tasks t ${ whereClause } ` , params ) ;
const total = Number ( totalRow ? . total || 0 ) ;
const tasks = await all (
`
SELECT
t . id , t . name as taskName , s . name as subjectName , s . total_score as totalScore ,
t . start_at as startAt , t . end_at as endAt
FROM exam_tasks t
JOIN exam_subjects s ON t . subject_id = s . id
$ { whereClause }
ORDER BY t . end_at DESC
LIMIT ? OFFSET ?
` ,
[ . . . params , input . limit , offset ] ,
) ;
const data : Array < ActiveTaskStat & { status : '已完成' | '进行中' | '未开始' } > = [ ] ;
for ( const task of tasks ) {
const report = await this . getReport ( task . id ) ;
const stat = this . buildActiveTaskStat ( {
taskId : task.id ,
taskName : task.taskName ,
subjectName : task.subjectName ,
totalScore : Number ( task . totalScore ) || 0 ,
startAt : task.startAt ,
endAt : task.endAt ,
report ,
} ) ;
const startMs = new Date ( task . startAt ) . getTime ( ) ;
const endMs = new Date ( task . endAt ) . getTime ( ) ;
const status : '已完成' | '进行中' | '未开始' =
Number . isFinite ( endMs ) && endMs < nowMs
? '已完成'
: Number . isFinite ( startMs ) && startMs > nowMs
? '未开始'
: '进行中' ;
data . push ( { . . . stat , status } ) ;
}
return { data , total } ;
}
2025-12-18 19:07:21 +08:00
static async findById ( id : string ) : Promise < ExamTask | null > {
2025-12-19 16:02:38 +08:00
const sql = ` SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt, selection_config as selectionConfig FROM exam_tasks WHERE id = ? ` ;
2025-12-18 19:07:21 +08:00
const row = await get ( sql , [ id ] ) ;
return row || null ;
}
static async create ( data : {
name : string ;
subjectId : string ;
startAt : string ;
endAt : string ;
userIds : string [ ] ;
2025-12-19 16:02:38 +08:00
selectionConfig? : string ;
2025-12-18 19:07:21 +08:00
} ) : 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 = `
2025-12-19 16:02:38 +08:00
INSERT INTO exam_tasks ( id , name , subject_id , start_at , end_at , selection_config )
VALUES ( ? , ? , ? , ? , ? , ? )
2025-12-18 19:07:21 +08:00
` ;
const sqlTaskUser = `
INSERT INTO exam_task_users ( id , task_id , user_id )
VALUES ( ? , ? , ? )
` ;
2025-12-19 16:02:38 +08:00
await run ( sqlTask , [ id , data . name . trim ( ) , data . subjectId , data . startAt , data . endAt , data . selectionConfig || null ] ) ;
2025-12-18 19:07:21 +08:00
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 [ ] ;
2025-12-19 16:02:38 +08:00
selectionConfig? : string ;
2025-12-18 19:07:21 +08:00
} ) : 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 ( '科目不存在' ) ;
2025-12-19 16:02:38 +08:00
await run ( ` UPDATE exam_tasks SET name = ?, subject_id = ?, start_at = ?, end_at = ?, selection_config = ? WHERE id = ? ` , [
2025-12-18 19:07:21 +08:00
data . name . trim ( ) ,
data . subjectId ,
data . startAt ,
data . endAt ,
2025-12-19 16:02:38 +08:00
data . selectionConfig || null ,
2025-12-18 19:07:21 +08:00
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 ] ) ;
}
2025-12-19 00:58:58 +08:00
static async getTaskUsers ( taskId : string ) : Promise < string [ ] > {
const rows = await all ( `
SELECT user_id as userId
FROM exam_task_users
WHERE task_id = ?
` , [taskId]);
return rows . map ( row = > row . userId ) ;
}
2025-12-18 19:07:21 +08:00
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 ( '用户未被分派到此任务' ) ;
2025-12-25 21:54:52 +08:00
const attemptRow = await get (
` SELECT COUNT(*) as count FROM quiz_records WHERE user_id = ? AND task_id = ? ` ,
[ userId , taskId ] ,
) ;
const usedAttempts = Number ( attemptRow ? . count ) || 0 ;
if ( usedAttempts >= 3 ) throw new Error ( '考试次数已用尽' ) ;
2025-12-18 19:07:21 +08:00
const subject = await import ( './examSubject' ) . then ( ( { ExamSubjectModel } ) = > ExamSubjectModel . findById ( task . subjectId ) ) ;
if ( ! subject ) throw new Error ( '科目不存在' ) ;
2025-12-26 18:39:17 +08:00
const { ExamSubjectModel } = await import ( './examSubject' ) ;
return await ExamSubjectModel . generateQuizQuestions ( subject ) ;
2025-12-18 19:07:21 +08:00
}
2025-12-25 21:54:52 +08:00
static async getUserTasks ( userId : string ) : Promise < UserExamTask [ ] > {
2025-12-18 19:07:21 +08:00
const now = new Date ( ) . toISOString ( ) ;
const rows = await all ( `
2025-12-25 21:54:52 +08:00
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 ,
s . total_score as totalScore ,
s . duration_minutes as timeLimitMinutes ,
COALESCE ( q . usedAttempts , 0 ) as usedAttempts ,
3 as maxAttempts ,
COALESCE ( q . bestScore , 0 ) as bestScore
2025-12-18 19:07:21 +08:00
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
2025-12-25 21:54:52 +08:00
LEFT JOIN (
SELECT task_id , COUNT ( * ) as usedAttempts , MAX ( total_score ) as bestScore
FROM quiz_records
WHERE user_id = ?
GROUP BY task_id
) q ON q . task_id = t . id
WHERE tu . user_id = ? AND t . start_at <= ? AND t . end_at >= ?
ORDER BY t . start_at ASC , t . end_at ASC
` , [userId, userId, now, now]);
2025-12-18 19:07:21 +08:00
return rows . map ( row = > ( {
id : row.id ,
name : row.name ,
2025-12-25 21:54:52 +08:00
subjectId : row.subjectId ,
startAt : row.startAt ,
endAt : row.endAt ,
createdAt : row.createdAt ,
2025-12-18 19:07:21 +08:00
subjectName : row.subjectName ,
2025-12-25 21:54:52 +08:00
totalScore : Number ( row . totalScore ) || 0 ,
timeLimitMinutes : Number ( row . timeLimitMinutes ) || 0 ,
usedAttempts : Number ( row . usedAttempts ) || 0 ,
maxAttempts : Number ( row . maxAttempts ) || 3 ,
bestScore : Number ( row . bestScore ) || 0 ,
2025-12-18 19:07:21 +08:00
} ) ) ;
}
2025-12-23 00:35:57 +08:00
}