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
}
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-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
}
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 ( '用户未被分派到此任务' ) ;
const subject = await import ( './examSubject' ) . then ( ( { ExamSubjectModel } ) = > ExamSubjectModel . findById ( task . subjectId ) ) ;
if ( ! subject ) throw new Error ( '科目不存在' ) ;
const { QuestionModel } = await import ( './question' ) ;
2025-12-19 00:58:58 +08:00
let questions : Awaited < ReturnType < typeof QuestionModel.getRandomQuestions > > = [ ] ;
// 构建包含所有类别的数组,根据比重重复对应次数
const allCategories : string [ ] = [ ] ;
for ( const [ category , catRatio ] of Object . entries ( subject . categoryRatios ) ) {
if ( catRatio > 0 ) {
// 根据比重计算该类别应占的总题目数比例
const count = Math . round ( ( catRatio / 100 ) * 100 ) ; // 放大100倍避免小数问题
for ( let i = 0 ; i < count ; i ++ ) {
allCategories . push ( category ) ;
}
}
}
// 确保总题目数至少为1
if ( allCategories . length === 0 ) {
allCategories . push ( '通用' ) ;
}
// 按题型分配题目
for ( const [ type , typeRatio ] of Object . entries ( subject . typeRatios ) ) {
if ( typeRatio <= 0 ) continue ;
// 计算该题型应占的总分
const targetTypeScore = Math . round ( ( typeRatio / 100 ) * subject . totalScore ) ;
let currentTypeScore = 0 ;
let typeQuestions : Awaited < ReturnType < typeof QuestionModel.getRandomQuestions > > = [ ] ;
// 尝试获取足够分数的题目
while ( currentTypeScore < targetTypeScore ) {
// 随机选择一个类别
const randomCategory = allCategories [ Math . floor ( Math . random ( ) * allCategories . length ) ] ;
// 获取该类型和类别的随机题目
const availableQuestions = await QuestionModel . getRandomQuestions (
type as any ,
10 , // 一次获取多个,提高效率
[ randomCategory ]
) ;
if ( availableQuestions . length === 0 ) {
break ; // 该类型/类别没有题目,跳过
}
// 过滤掉已选题目
const availableUnselected = availableQuestions . filter ( q = >
! questions . some ( selected = > selected . id === q . id )
) ;
if ( availableUnselected . length === 0 ) {
break ; // 没有可用的新题目了
}
// 选择分数最接近剩余需求的题目
const remainingForType = targetTypeScore - currentTypeScore ;
const selectedQuestion = availableUnselected . reduce ( ( prev , curr ) = > {
const prevDiff = Math . abs ( remainingForType - prev . score ) ;
const currDiff = Math . abs ( remainingForType - curr . score ) ;
return currDiff < prevDiff ? curr : prev ;
} ) ;
// 添加到题型题目列表
typeQuestions . push ( selectedQuestion ) ;
currentTypeScore += selectedQuestion . score ;
// 防止无限循环
if ( typeQuestions . length > 100 ) {
break ;
}
}
questions . push ( . . . typeQuestions ) ;
}
// 如果总分不足,尝试补充题目
let totalScore = questions . reduce ( ( sum , q ) = > sum + q . score , 0 ) ;
while ( totalScore < subject . totalScore ) {
// 获取所有类型的随机题目
2025-12-21 01:56:54 +08:00
const allTypes = Object . keys ( subject . typeRatios ) . filter ( type = > subject . typeRatios [ type as keyof typeof subject . typeRatios ] > 0 ) ;
2025-12-19 00:58:58 +08:00
if ( allTypes . length === 0 ) break ;
const randomType = allTypes [ Math . floor ( Math . random ( ) * allTypes . length ) ] ;
const availableQuestions = await QuestionModel . getRandomQuestions (
randomType as any ,
10 ,
allCategories
) ;
if ( availableQuestions . length === 0 ) break ;
// 过滤掉已选题目
const availableUnselected = availableQuestions . filter ( q = >
! questions . some ( selected = > selected . id === q . id )
) ;
if ( availableUnselected . length === 0 ) break ;
// 选择分数最接近剩余需求的题目
const remainingScore = subject . totalScore - totalScore ;
const selectedQuestion = availableUnselected . reduce ( ( prev , curr ) = > {
const prevDiff = Math . abs ( remainingScore - prev . score ) ;
const currDiff = Math . abs ( remainingScore - curr . score ) ;
return currDiff < prevDiff ? curr : prev ;
} ) ;
questions . push ( selectedQuestion ) ;
totalScore += selectedQuestion . score ;
// 防止无限循环
if ( questions . length > 200 ) {
break ;
}
}
// 如果总分超过,尝试移除一些题目
while ( totalScore > subject . totalScore ) {
// 找到最接近剩余差值的题目
const excessScore = totalScore - subject . totalScore ;
let closestIndex = - 1 ;
let closestDiff = Infinity ;
for ( let i = 0 ; i < questions . length ; i ++ ) {
const diff = Math . abs ( questions [ i ] . score - excessScore ) ;
if ( diff < closestDiff ) {
closestDiff = diff ;
closestIndex = i ;
}
}
if ( closestIndex === - 1 ) break ;
// 移除该题目
totalScore -= questions [ closestIndex ] . score ;
questions . splice ( closestIndex , 1 ) ;
2025-12-18 19:07:21 +08:00
}
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
2025-12-19 00:58:58 +08:00
WHERE tu . user_id = ? AND t . start_at <= ?
2025-12-18 19:07:21 +08:00
ORDER BY t . start_at DESC
2025-12-19 00:58:58 +08:00
` , [userId, now]);
2025-12-18 19:07:21 +08:00
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
} ) ) ;
}
}