@@ -0,0 +1,714 @@
export function createAdminRoutes ( context ) {
const {
database ,
readDb ,
sendJson ,
sendError ,
readJson ,
readBodyBuffer ,
sendWorkbook ,
currentUser ,
safeUser ,
requireUser ,
hasPermission ,
requirePermission ,
profileInScope ,
registrationInScope ,
adminScopeLabel ,
adminsForStep ,
activeWorkflow ,
createWorkflowSubmission ,
workflowView ,
pendingWorkflow ,
candidateSequence ,
generateCandidateNumber ,
cleanText ,
centerScopeProfile ,
workflowScopeProfile ,
candidateAccountBatchView ,
centerChangeView ,
parseCenterChange ,
maskId ,
publicExam ,
examRegistrationView ,
logAction ,
excelResourceNames ,
excelRowsForResource ,
importExcelResource ,
admitCardHtml ,
hashPassword ,
verifyPassword ,
randomBytes ,
uid ,
nowIso ,
sessions ,
buildWorkbook ,
hasExcelResource ,
parseWorkbook ,
adminLevelNames ,
permissionsByLevel
} = context ;
async function handleAdmin ( request , response , pathname ) {
if ( ! pathname . startsWith ( '/api/admin/' ) ) return false ;
const user = await requireUser ( request , response , 'admin' ) ;
if ( ! user ) return true ;
const db = await readDb ( ) ;
if ( request . method === 'GET' && pathname === '/api/admin/context' ) {
return sendJson ( response , 200 , {
ok : true ,
admin : safeUser ( user ) ,
adminLevelName : adminLevelNames [ user . adminLevel || 'super' ] ,
permissions : permissionsByLevel [ user . adminLevel || 'super' ] ,
scopeLabel : adminScopeLabel ( db , user ) ,
schools : db . schools ,
classes : db . classes
} ) ;
}
const excelMatch = pathname . match ( /^\/api\/admin\/excel\/(classes|class_admins|account_quotas|account_results|candidates|centers|results)$/ ) ;
if ( excelMatch && request . method === 'GET' ) {
const resource = excelMatch [ 1 ] ;
if ( ! hasExcelResource ( resource ) ) return sendError ( response , 404 , 'Excel 数据类型不存在' ) ;
if ( [ 'classes' , 'class_admins' , 'account_quotas' , 'account_results' ] . includes ( resource ) && ! [ 'school' , 'super' ] . includes ( user . adminLevel ) ) return sendError ( response , 403 , '当前账号不能导出该数据' ) ;
if ( resource === 'centers' && ! hasPermission ( user , 'centers.read' ) ) return sendError ( response , 403 , '当前账号不能导出考点考场' ) ;
if ( resource === 'candidates' && ! hasPermission ( user , 'candidates.read' ) ) return sendError ( response , 403 , '当前账号不能导出考生资料' ) ;
if ( resource === 'results' && ! hasPermission ( user , 'results.read' ) ) return sendError ( response , 403 , '当前账号不能导出成绩' ) ;
const requestUrl = new URL ( request . url , ` http:// ${ request . headers . host || '127.0.0.1' } ` ) ;
const template = requestUrl . searchParams . get ( 'template' ) === '1' ;
const rows = template ? [ ] : excelRowsForResource ( db , user , resource , requestUrl . searchParams ) ;
const subtitle = user . adminLevel === 'super' ? '全部数据范围' : adminScopeLabel ( db , user ) ;
const buffer = Buffer . from ( await buildWorkbook ( resource , rows , { template , subtitle } ) ) ;
return sendWorkbook ( response , buffer , ` ${ excelResourceNames [ resource ] } - ${ template ? '导入模板' : '导出' } - ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .xlsx ` ) ;
}
if ( excelMatch && request . method === 'POST' ) {
const resource = excelMatch [ 1 ] ;
if ( resource === 'account_results' ) return sendError ( response , 400 , '账号结果清单只支持导出' ) ;
const rows = await parseWorkbook ( resource , await readBodyBuffer ( request ) ) ;
const result = await importExcelResource ( db , user , resource , rows ) ;
return sendJson ( response , 200 , { ok : true , ... result } ) ;
}
if ( pathname === '/api/admin/school-organization' && request . method === 'GET' ) {
if ( user . adminLevel !== 'school' ) return sendError ( response , 403 , '只有校级管理员可以维护本校组织' ) ;
const school = db . schools . find ( item => item . id === user . schoolId ) ;
const classes = db . classes . filter ( item => item . schoolId === user . schoolId ) . map ( item => ( {
... item ,
candidateCount : db . candidateProfiles . filter ( profile => profile . classId === item . id ) . length ,
admins : db . users . filter ( admin => admin . role === 'admin' && admin . adminLevel === 'class' && admin . classId === item . id ) . map ( admin => ( { ... safeUser ( admin ) , active : admin . active } ) )
} ) ) ;
return sendJson ( response , 200 , { ok : true , school , classes } ) ;
}
if ( pathname === '/api/admin/classes' && request . method === 'POST' ) {
if ( user . adminLevel !== 'school' ) return sendError ( response , 403 , '只有校级管理员可以新增本校班级' ) ;
const body = await readJson ( request ) ;
const name = cleanText ( body . name , 100 ) ; const grade = cleanText ( body . grade , 60 ) ;
if ( ! name || ! grade ) return sendError ( response , 400 , '年级和班级名称不能为空' ) ;
if ( db . classes . some ( item => item . schoolId === user . schoolId && item . name === name ) ) return sendError ( response , 409 , '本校已存在同名班级' ) ;
const schoolClass = { id : uid ( 'class' ) , schoolId : user . schoolId , name , grade , active : body . active !== false } ;
await database . saveSchoolClass ( schoolClass , true , logAction ( db , user , '新增本校班级' , ` ${ grade } · ${ name } ` ) ) ;
return sendJson ( response , 201 , { ok : true , schoolClass } ) ;
}
const classMatch = pathname . match ( /^\/api\/admin\/classes\/([^/]+)$/ ) ;
if ( classMatch && request . method === 'PATCH' ) {
if ( user . adminLevel !== 'school' ) return sendError ( response , 403 , '只有校级管理员可以维护本校班级' ) ;
const body = await readJson ( request ) ;
const schoolClass = db . classes . find ( item => item . id === classMatch [ 1 ] && item . schoolId === user . schoolId ) ;
if ( ! schoolClass ) return sendError ( response , 404 , '班级不存在' ) ;
const name = cleanText ( body . name ? ? schoolClass . name , 100 ) ; const grade = cleanText ( body . grade ? ? schoolClass . grade , 60 ) ;
if ( ! name || ! grade ) return sendError ( response , 400 , '年级和班级名称不能为空' ) ;
if ( db . classes . some ( item => item . id !== schoolClass . id && item . schoolId === user . schoolId && item . name === name ) ) return sendError ( response , 409 , '本校已存在同名班级' ) ;
Object . assign ( schoolClass , { name , grade , active : body . active == null ? schoolClass . active : Boolean ( body . active ) } ) ;
await database . saveSchoolClass ( schoolClass , false , logAction ( db , user , '更新本校班级' , ` ${ grade } · ${ name } · ${ schoolClass . active ? '启用' : '停用' } ` ) ) ;
return sendJson ( response , 200 , { ok : true , schoolClass } ) ;
}
if ( pathname === '/api/admin/admins' && request . method === 'GET' ) {
if ( ! [ 'super' , 'school' ] . includes ( user . adminLevel ) ) return sendError ( response , 403 , '当前账号不能管理管理员' ) ;
const admins = db . users . filter ( item => item . role === 'admin' && ( user . adminLevel === 'super' || ( item . adminLevel === 'class' && item . schoolId === user . schoolId ) ) ) . map ( item => ( {
... safeUser ( item ) ,
active : item . active ,
levelName : adminLevelNames [ item . adminLevel ] ,
schoolName : db . schools . find ( school => school . id === item . schoolId ) ? . name || '' ,
className : db . classes . find ( schoolClass => schoolClass . id === item . classId ) ? . name || ''
} ) ) ;
return sendJson ( response , 200 , { ok : true , admins , schools : db . schools , classes : db . classes , selfRegistrationEnabled : db . settings . selfRegistrationEnabled } ) ;
}
if ( pathname === '/api/admin/admins' && request . method === 'POST' ) {
const body = await readJson ( request ) ;
const username = cleanText ( body . username , 50 ) ;
const password = String ( body . password || '' ) ;
const displayName = cleanText ( body . displayName , 50 ) ;
const adminLevel = user . adminLevel === 'school' ? 'class' : cleanText ( body . adminLevel , 20 ) ;
if ( ! [ 'super' , 'school' ] . includes ( user . adminLevel ) ) return sendError ( response , 403 , '当前账号不能创建管理员' ) ;
if ( ! username || ! displayName || password . length < 8 || ! [ 'super' , 'school' , 'class' ] . includes ( adminLevel ) ) return sendError ( response , 400 , '请完整填写管理员账号、姓名、层级和至少 8 位密码' ) ;
if ( db . users . some ( item => item . username . toLowerCase ( ) === username . toLowerCase ( ) ) ) return sendError ( response , 409 , '该登录账号已存在' ) ;
const schoolId = adminLevel === 'super' ? null : user . adminLevel === 'school' ? user . schoolId : cleanText ( body . schoolId , 64 ) ;
const classId = adminLevel === 'class' ? cleanText ( body . classId , 64 ) : null ;
if ( adminLevel !== 'super' && ! db . schools . some ( item => item . id === schoolId ) ) return sendError ( response , 400 , '校级和班级管理员必须绑定学校' ) ;
if ( adminLevel === 'class' && ! db . classes . some ( item => item . id === classId && item . schoolId === schoolId ) ) return sendError ( response , 400 , '请选择该学校下的有效班级' ) ;
const created = { id : uid ( 'usr' ) , username , passwordHash : hashPassword ( password ) , role : 'admin' , adminLevel , schoolId , classId , displayName , active : true , createdAt : nowIso ( ) } ;
const log = logAction ( db , user , '创建管理员' , ` ${ displayName } · ${ adminLevelNames [ adminLevel ] } ` ) ;
await database . createAdmin ( created , log ) ;
return sendJson ( response , 201 , { ok : true , admin : safeUser ( created ) } ) ;
}
const adminMatch = pathname . match ( /^\/api\/admin\/admins\/([^/]+)$/ ) ;
if ( adminMatch && request . method === 'PATCH' ) {
if ( user . adminLevel !== 'school' ) return sendError ( response , 403 , '只有校级管理员可以维护本校班级管理员' ) ;
const body = await readJson ( request ) ;
const target = db . users . find ( item => item . id === adminMatch [ 1 ] && item . role === 'admin' && item . adminLevel === 'class' && item . schoolId === user . schoolId ) ;
if ( ! target ) return sendError ( response , 404 , '班级管理员不存在' ) ;
const schoolClass = db . classes . find ( item => item . id === cleanText ( body . classId || target . classId , 64 ) && item . schoolId === user . schoolId ) ;
if ( ! schoolClass ) return sendError ( response , 400 , '请选择本校有效班级' ) ;
const password = String ( body . password || '' ) ;
if ( password && password . length < 8 ) return sendError ( response , 400 , '重置密码至少 8 位' ) ;
Object . assign ( target , { displayName : cleanText ( body . displayName || target . displayName , 50 ) , classId : schoolClass . id , active : body . active == null ? target . active : Boolean ( body . active ) } ) ;
if ( password ) target . passwordHash = hashPassword ( password ) ;
await database . updateAdmin ( target , Boolean ( password ) , logAction ( db , user , '维护班级管理员' , ` ${ target . displayName } · ${ schoolClass . name } ` ) ) ;
return sendJson ( response , 200 , { ok : true , admin : safeUser ( target ) } ) ;
}
if ( pathname === '/api/admin/settings/self-registration' && request . method === 'PUT' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const enabled = Boolean ( body . enabled ) ;
const log = logAction ( db , user , enabled ? '开启自主注册' : '关闭自主注册' , enabled ? '考生可从公开入口申请报名号' : '仅允许使用学校下发的报名号登录' ) ;
await database . updateRegistrationSetting ( enabled , log ) ;
return sendJson ( response , 200 , { ok : true , enabled } ) ;
}
if ( pathname === '/api/admin/candidate-account-batches' && request . method === 'GET' ) {
if ( ! requirePermission ( user , response , 'candidates.write' ) ) return true ;
if ( ! [ 'school' , 'super' ] . includes ( user . adminLevel ) ) return sendError ( response , 403 , '只有校级管理员可以申领批量报名号' ) ;
const batches = db . candidateAccountBatches
. filter ( item => user . adminLevel === 'super' || item . schoolId === user . schoolId )
. sort ( ( a , b ) => new Date ( b . createdAt ) - new Date ( a . createdAt ) )
. map ( item => candidateAccountBatchView ( db , item ) ) ;
const classes = db . classes . filter ( item => item . active && ( user . adminLevel === 'super' || item . schoolId === user . schoolId ) ) ;
return sendJson ( response , 200 , { ok : true , batches , classes , schools : db . schools . filter ( item => item . active ) } ) ;
}
if ( pathname === '/api/admin/candidate-account-batches' && request . method === 'POST' ) {
if ( user . adminLevel !== 'school' || ! requirePermission ( user , response , 'candidates.write' ) ) return user . adminLevel === 'school' ? true : sendError ( response , 403 , '批量报名号由校级管理员发起申领' ) ;
const body = await readJson ( request ) ;
const requestedQuotas = Array . isArray ( body . quotas ) ? body . quotas : [ ] ;
const quotas = requestedQuotas . map ( item => ( { classId : cleanText ( item . classId , 64 ) , count : Number ( item . count ) } ) ) . filter ( item => item . count > 0 ) ;
if ( ! quotas . length ) return sendError ( response , 400 , '请至少为一个班级填写申领数量' ) ;
if ( new Set ( quotas . map ( item => item . classId ) ) . size !== quotas . length ) return sendError ( response , 400 , '同一班级只能填写一次申领数量' ) ;
if ( quotas . some ( item => ! Number . isInteger ( item . count ) || item . count < 1 || item . count > 200 ) ) return sendError ( response , 400 , '每个班级一次可申领 1—200 个报名号' ) ;
if ( quotas . some ( item => ! db . classes . some ( schoolClass => schoolClass . id === item . classId && schoolClass . schoolId === user . schoolId && schoolClass . active ) ) ) return sendError ( response , 400 , '只能为本校有效班级申领报名号' ) ;
const totalCount = quotas . reduce ( ( sum , item ) => sum + item . count , 0 ) ;
if ( totalCount > 500 ) return sendError ( response , 400 , '单个批次最多申领 500 个报名号' ) ;
const batch = { id : uid ( 'account_batch' ) , schoolId : user . schoolId , requestedBy : user . id , status : 'pending' , reviewNote : '' , createdAt : nowIso ( ) , reviewedAt : null } ;
const items = [ ] ;
let position = 1 ;
for ( const quota of quotas ) for ( let index = 0 ; index < quota . count ; index += 1 ) {
items . push ( { id : uid ( 'account_batch_item' ) , batchId : batch . id , classId : quota . classId , position , candidateNumber : '' , initialPassword : '' , userId : null , createdAt : null } ) ;
position += 1 ;
}
const { instance , action } = createWorkflowSubmission ( db , 'candidate_account_batch' , batch . id , centerScopeProfile ( db , user . schoolId ) , user . id ) ;
const quotaSummary = quotas . map ( item => ` ${ db . classes . find ( entry => entry . id === item . classId ) ? . name } ${ item . count } 人 ` ) . join ( '; ' ) ;
const log = logAction ( db , user , '提交批量报名号申领' , ` ${ totalCount } 个账户 · ${ quotaSummary } ` ) ;
await database . createCandidateAccountBatch ( batch , items , instance , action , log ) ;
const fresh = await readDb ( ) ;
return sendJson ( response , 202 , { ok : true , batch : candidateAccountBatchView ( fresh , fresh . candidateAccountBatches . find ( item => item . id === batch . id ) ) } ) ;
}
const accountBatchMatch = pathname . match ( /^\/api\/admin\/candidate-account-batches\/([^/]+)$/ ) ;
if ( accountBatchMatch && request . method === 'PATCH' ) {
if ( ! requirePermission ( user , response , 'candidates.write' ) ) return true ;
const body = await readJson ( request ) ;
if ( ! [ 'approved' , 'rejected' ] . includes ( body . status ) ) return sendError ( response , 400 , '审批状态无效' ) ;
const batch = db . candidateAccountBatches . find ( item => item . id === accountBatchMatch [ 1 ] && item . status === 'pending' ) ;
if ( ! batch ) return sendError ( response , 404 , '待审批的批量报名号申请不存在' ) ;
const instance = pendingWorkflow ( db , 'candidate_account_batch' , batch . id ) ;
const workflow = instance && db . workflows . find ( item => item . id === instance . workflowId ) ;
const step = workflow ? . steps . find ( item => item . position === instance . currentStep ) ;
if ( ! instance || ! workflow || ! step ) return sendError ( response , 409 , '批量报名号审批流程状态异常' ) ;
if ( user . adminLevel !== 'super' && ( instance . assigneeId !== user . id || step . adminLevel !== user . adminLevel ) ) return sendError ( response , 403 , '该流程当前未分配给你,可由当前处理人转交' ) ;
const note = cleanText ( body . reviewNote , 300 ) ;
const action = { id : uid ( 'flow_action' ) , instanceId : instance . id , actorId : user . id , action : body . status === 'approved' ? 'approve' : 'reject' , note , fromAssigneeId : instance . assigneeId , toAssigneeId : null , createdAt : nowIso ( ) } ;
const log = logAction ( db , user , body . status === 'approved' ? '审批批量报名号申领' : '退回批量报名号申领' , ` ${ db . schools . find ( item => item . id === batch . schoolId ) ? . name } · ${ note || '无备注' } ` ) ;
if ( body . status === 'rejected' ) {
instance . status = 'rejected' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
batch . status = 'rejected' ; batch . reviewNote = note ; batch . reviewedAt = nowIso ( ) ;
await database . processWorkflow ( instance , action , batch , log ) ;
} else if ( instance . currentStep < workflow . steps . length ) {
const nextStep = workflow . steps . find ( item => item . position === instance . currentStep + 1 ) ;
const nextAssignee = adminsForStep ( db , nextStep . adminLevel , centerScopeProfile ( db , batch . schoolId ) ) [ 0 ] ;
if ( ! nextAssignee ) return sendError ( response , 409 , ` 没有可承接“ ${ nextStep . name } ”的管理员 ` ) ;
instance . currentStep += 1 ; instance . assigneeId = nextAssignee . id ; action . toAssigneeId = nextAssignee . id ;
batch . reviewNote = note ;
await database . processWorkflow ( instance , action , batch , log ) ;
} else {
const batchItems = db . candidateAccountBatchItems . filter ( item => item . batchId === batch . id ) . sort ( ( a , b ) => a . position - b . position ) ;
if ( ! batchItems . length || batchItems . some ( item => item . userId || item . candidateNumber ) ) return sendError ( response , 409 , '批次明细异常或已经生成过账号' ) ;
const generationDb = { ... db , users : [ ... db . users ] } ;
const users = [ ] ;
const profiles = [ ] ;
for ( const [ index , item ] of batchItems . entries ( ) ) {
const schoolClass = db . classes . find ( entry => entry . id === item . classId && entry . schoolId === batch . schoolId ) ;
if ( ! schoolClass ) return sendError ( response , 409 , '批次包含无效班级,无法生成账号' ) ;
const generated = generateCandidateNumber ( generationDb , { schoolId : batch . schoolId , classId : item . classId , gender : '' } ) ;
const userId = uid ( 'usr' ) ;
const initialPassword = ` Init- ${ randomBytes ( 6 ) . toString ( 'base64url' ) } ` ;
const displayName = ` 待补录考生 ${ String ( index + 1 ) . padStart ( 3 , '0' ) } ` ;
const candidateUser = { id : userId , username : generated . number , candidateNumber : generated . number , passwordHash : hashPassword ( initialPassword ) , role : 'candidate' , displayName , schoolId : batch . schoolId , classId : item . classId , active : true , mustChangePassword : true , createdAt : nowIso ( ) } ;
const profile = { id : uid ( 'profile' ) , userId , name : displayName , gender : '' , idNumber : ` PENDING- ${ userId } ` , phone : '' , email : '' , school : db . schools . find ( entry => entry . id === batch . schoolId ) ? . name || '' , grade : schoolClass . name , schoolId : batch . schoolId , classId : item . classId , address : '' , emergencyContact : '' , emergencyPhone : '' , nativePlace : '' , birthDate : '' , ethnicity : '' , postalCode : '' , guardianName : '' , guardianPhone : '' , profileCompleted : false , status : 'pending' , reviewNote : '' , updatedAt : nowIso ( ) } ;
item . candidateNumber = generated . number ; item . initialPassword = initialPassword ; item . userId = userId ; item . createdAt = nowIso ( ) ;
users . push ( candidateUser ) ; profiles . push ( profile ) ; generationDb . users . push ( candidateUser ) ;
}
instance . status = 'approved' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
batch . status = 'approved' ; batch . reviewNote = note ; batch . reviewedAt = nowIso ( ) ;
await database . completeCandidateAccountBatch ( batch , batchItems , users , profiles , instance , action , log ) ;
}
const fresh = await readDb ( ) ;
return sendJson ( response , 200 , { ok : true , batch : candidateAccountBatchView ( fresh , fresh . candidateAccountBatches . find ( item => item . id === batch . id ) ) } ) ;
}
if ( pathname === '/api/admin/centers' && request . method === 'GET' ) {
if ( ! requirePermission ( user , response , 'centers.read' ) ) return true ;
const centers = db . testCenters . filter ( item => user . adminLevel === 'super' || item . schoolId === user . schoolId ) . map ( item => ( {
... item ,
schoolName : db . schools . find ( school => school . id === item . schoolId ) ? . name || '' ,
rooms : db . testRooms . filter ( room => room . centerId === item . id ) ,
totalCapacity : db . testRooms . filter ( room => room . centerId === item . id && room . status === 'active' ) . reduce ( ( sum , room ) => sum + Number ( room . capacity || 0 ) , 0 ) ,
pendingChange : db . centerChangeRequests . some ( change => change . centerId === item . id && change . status === 'pending' )
} ) ) ;
const changeRequests = db . centerChangeRequests
. filter ( item => user . adminLevel === 'super' || item . schoolId === user . schoolId )
. sort ( ( a , b ) => new Date ( b . createdAt ) - new Date ( a . createdAt ) )
. map ( item => centerChangeView ( db , item ) ) ;
return sendJson ( response , 200 , { ok : true , centers , changeRequests , schools : user . adminLevel === 'super' ? db . schools : db . schools . filter ( item => item . id === user . schoolId ) } ) ;
}
if ( pathname === '/api/admin/centers' && request . method === 'POST' ) {
if ( ! requirePermission ( user , response , 'centers.write' ) ) return true ;
const body = await readJson ( request ) ;
const schoolId = user . adminLevel === 'super' ? cleanText ( body . schoolId , 64 ) : user . schoolId ;
if ( ! db . schools . some ( item => item . id === schoolId ) ) return sendError ( response , 400 , '考点必须归属有效学校' ) ;
const parsed = parseCenterChange ( db , body , schoolId ) ;
const change = { id : uid ( 'center_change' ) , centerId : null , schoolId , requestType : 'create' , ... parsed . center , status : 'pending' , reviewNote : '' , requestedBy : user . id , createdAt : nowIso ( ) , reviewedAt : null } ;
const { instance , action } = createWorkflowSubmission ( db , 'center_change' , change . id , centerScopeProfile ( db , schoolId ) , user . id ) ;
const log = logAction ( db , user , '提交新增考点审批' , ` ${ change . name } · ${ parsed . rooms . length } 个考场 ` ) ;
await database . createCenterChangeRequest ( change , parsed . rooms , instance , action , log ) ;
return sendJson ( response , 202 , { ok : true , changeRequest : { ... change , rooms : parsed . rooms , workflow : workflowView ( { ... db , workflowActions : [ ... db . workflowActions , action ] } , instance ) } } ) ;
}
const centerMatch = pathname . match ( /^\/api\/admin\/centers\/([^/]+)$/ ) ;
if ( centerMatch && request . method === 'PATCH' ) {
if ( ! requirePermission ( user , response , 'centers.write' ) ) return true ;
const body = await readJson ( request ) ;
const center = db . testCenters . find ( item => item . id === centerMatch [ 1 ] ) ;
if ( ! center ) return sendError ( response , 404 , '考点不存在' ) ;
if ( user . adminLevel !== 'super' && center . schoolId !== user . schoolId ) return sendError ( response , 403 , '只能维护本校考点' ) ;
if ( db . centerChangeRequests . some ( item => item . centerId === center . id && item . status === 'pending' ) ) return sendError ( response , 409 , '该考点已有待审批变更,请处理完成后再提交' ) ;
const parsed = parseCenterChange ( db , body , center . schoolId , center ) ;
const change = { id : uid ( 'center_change' ) , centerId : center . id , schoolId : center . schoolId , requestType : 'update' , ... parsed . center , status : 'pending' , reviewNote : '' , requestedBy : user . id , createdAt : nowIso ( ) , reviewedAt : null } ;
const { instance , action } = createWorkflowSubmission ( db , 'center_change' , change . id , centerScopeProfile ( db , center . schoolId ) , user . id ) ;
const log = logAction ( db , user , '提交考点变更审批' , ` ${ change . name } · ${ parsed . rooms . length } 个考场 ` ) ;
await database . createCenterChangeRequest ( change , parsed . rooms , instance , action , log ) ;
return sendJson ( response , 202 , { ok : true , changeRequest : { ... change , rooms : parsed . rooms , workflow : workflowView ( { ... db , workflowActions : [ ... db . workflowActions , action ] } , instance ) } } ) ;
}
const centerChangeMatch = pathname . match ( /^\/api\/admin\/center-change-requests\/([^/]+)$/ ) ;
if ( centerChangeMatch && request . method === 'PATCH' ) {
if ( ! requirePermission ( user , response , 'centers.write' ) ) return true ;
const body = await readJson ( request ) ;
if ( ! [ 'approved' , 'rejected' ] . includes ( body . status ) ) return sendError ( response , 400 , '审批状态无效' ) ;
const change = db . centerChangeRequests . find ( item => item . id === centerChangeMatch [ 1 ] && item . status === 'pending' ) ;
if ( ! change ) return sendError ( response , 404 , '待审批的考点变更不存在' ) ;
if ( user . adminLevel !== 'super' && change . schoolId !== user . schoolId ) return sendError ( response , 403 , '该变更不在你的学校范围内' ) ;
const instance = pendingWorkflow ( db , 'center_change' , change . id ) ;
const workflow = instance && db . workflows . find ( item => item . id === instance . workflowId ) ;
const step = workflow ? . steps . find ( item => item . position === instance . currentStep ) ;
if ( ! instance || ! workflow || ! step ) return sendError ( response , 409 , '考点变更审批流程状态异常' ) ;
if ( user . adminLevel !== 'super' && ( instance . assigneeId !== user . id || step . adminLevel !== user . adminLevel ) ) return sendError ( response , 403 , '该流程当前未分配给你,可由当前处理人转交' ) ;
const note = cleanText ( body . reviewNote , 300 ) ;
const action = { id : uid ( 'flow_action' ) , instanceId : instance . id , actorId : user . id , action : body . status === 'approved' ? 'approve' : 'reject' , note , fromAssigneeId : instance . assigneeId , toAssigneeId : null , createdAt : nowIso ( ) } ;
const log = logAction ( db , user , body . status === 'approved' ? '审批考点变更' : '退回考点变更' , ` ${ change . name } · ${ note || '无备注' } ` ) ;
if ( body . status === 'rejected' ) {
instance . status = 'rejected' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
change . status = 'rejected' ; change . reviewNote = note ; change . reviewedAt = nowIso ( ) ;
await database . applyCenterChange ( change , instance , action , null , [ ] , log ) ;
} else if ( instance . currentStep < workflow . steps . length ) {
const nextStep = workflow . steps . find ( item => item . position === instance . currentStep + 1 ) ;
const nextAssignee = adminsForStep ( db , nextStep . adminLevel , centerScopeProfile ( db , change . schoolId ) ) [ 0 ] ;
if ( ! nextAssignee ) return sendError ( response , 409 , ` 没有可承接“ ${ nextStep . name } ”的管理员 ` ) ;
instance . currentStep += 1 ; instance . assigneeId = nextAssignee . id ; action . toAssigneeId = nextAssignee . id ;
change . reviewNote = note ;
await database . processWorkflow ( instance , action , change , log ) ;
} else {
instance . status = 'approved' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
change . status = 'approved' ; change . reviewNote = note ; change . reviewedAt = nowIso ( ) ;
const centerId = change . centerId || uid ( 'center' ) ;
const proposedRooms = db . centerChangeRooms . filter ( item => item . requestId === change . id ) ;
const rooms = proposedRooms . map ( room => ( { ... room , id : room . roomId || uid ( 'room' ) , centerId } ) ) ;
const center = {
id : centerId , schoolId : change . schoolId , code : change . code , name : change . name , address : change . address ,
contact : change . contact , managerName : change . managerName , managerPhone : change . managerPhone ,
emergencyPhone : change . emergencyPhone , gateOpenTime : change . gateOpenTime , transport : change . transport ,
status : change . centerStatus , notes : change . notes ,
rooms : rooms . map ( room => ` ${ room . building } ${ room . name } ` ) . join ( '; ' ) , updatedAt : nowIso ( )
} ;
await database . applyCenterChange ( change , instance , action , center , rooms , log ) ;
}
return sendJson ( response , 200 , { ok : true , changeRequest : centerChangeView ( { ... db , workflowActions : [ ... db . workflowActions , action ] } , change ) } ) ;
}
if ( pathname === '/api/admin/number-rules' && request . method === 'GET' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const rule = db . numberRules . find ( item => item . active ) || null ;
const previewProfile = db . candidateProfiles [ 0 ] || { gender : '女' , schoolId : db . schools [ 0 ] ? . id } ;
let preview = '' ;
if ( rule ) preview = generateCandidateNumber ( db , previewProfile ) . number ;
return sendJson ( response , 200 , { ok : true , rules : db . numberRules , activeRule : rule , preview } ) ;
}
if ( pathname === '/api/admin/number-rules' && request . method === 'POST' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const allowedTypes = [ 'year' , 'school_code' , 'gender' , 'sequence' , 'literal' ] ;
const requested = Array . isArray ( body . segments ) ? body . segments : [ ] ;
if ( ! requested . length || requested . some ( item => ! allowedTypes . includes ( item . type ) ) || ! requested . some ( item => item . type === 'sequence' ) ) return sendError ( response , 400 , '报名号规则至少包含一个流水号段' ) ;
const existing = db . numberRules . find ( item => item . id === body . id ) ;
const rule = {
id : existing ? . id || uid ( 'rule' ) , name : cleanText ( body . name , 80 ) || '自定义报名号规则' , separator : cleanText ( body . separator , 3 ) ,
active : true , createdBy : user . id , updatedAt : nowIso ( ) , segments : requested . map ( ( item , index ) => ( {
id : uid ( 'segment' ) , position : index + 1 , type : item . type , value : cleanText ( item . value , 20 ) , width : Math . min ( 12 , Math . max ( 0 , Number ( item . width || 0 ) ) )
} ) )
} ;
const log = logAction ( db , user , '更新报名号规则' , ` ${ rule . name } · ${ rule . segments . map ( item => item . type ) . join ( ' + ' ) } ` ) ;
await database . saveNumberRule ( rule , ! existing , log ) ;
return sendJson ( response , 200 , { ok : true , rule } ) ;
}
if ( pathname === '/api/admin/workflows' && request . method === 'GET' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
return sendJson ( response , 200 , { ok : true , workflows : db . workflows } ) ;
}
const workflowDefinitionMatch = pathname . match ( /^\/api\/admin\/workflows\/(profile_change|registration_review|center_change|candidate_account_batch)$/ ) ;
if ( workflowDefinitionMatch && request . method === 'PUT' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const workflow = activeWorkflow ( db , workflowDefinitionMatch [ 1 ] ) ;
if ( ! workflow ) return sendError ( response , 404 , '审批流程不存在' ) ;
const steps = Array . isArray ( body . steps ) ? body . steps : [ ] ;
if ( ! steps . length || steps . some ( item => ! [ 'school' , 'super' ] . includes ( item . adminLevel ) ) ) return sendError ( response , 400 , '流程至少需要一个校级或超级管理员审批步骤' ) ;
if ( workflowDefinitionMatch [ 1 ] === 'candidate_account_batch' && steps . at ( - 1 ) ? . adminLevel !== 'super' ) return sendError ( response , 400 , '批量报名号申领的最终步骤必须由超级管理员审批' ) ;
workflow . name = cleanText ( body . name , 80 ) || workflow . name ;
workflow . updatedBy = user . id ;
workflow . updatedAt = nowIso ( ) ;
workflow . steps = steps . map ( ( item , index ) => ( { id : uid ( 'workflow_step' ) , position : index + 1 , name : cleanText ( item . name , 80 ) || ` 第 ${ index + 1 } 步 ` , adminLevel : item . adminLevel } ) ) ;
const log = logAction ( db , user , '修改审批流程' , ` ${ workflow . name } · ${ workflow . steps . length } 个步骤 ` ) ;
await database . saveWorkflow ( workflow , log ) ;
return sendJson ( response , 200 , { ok : true , workflow } ) ;
}
if ( pathname === '/api/admin/workflow-instances' && request . method === 'GET' ) {
if ( user . adminLevel === 'class' ) return sendError ( response , 403 , '班级管理员只读查看考生、成绩和报名状态' ) ;
const instances = db . workflowInstances . filter ( instance => {
if ( user . adminLevel === 'super' ) return true ;
const profile = workflowScopeProfile ( db , instance ) ;
return Boolean ( profile && profileInScope ( user , profile ) ) ;
} ) . map ( instance => {
const profile = workflowScopeProfile ( db , instance ) ;
const registration = instance . businessType === 'registration_review' ? db . registrations . find ( item => item . id === instance . businessId ) : null ;
const centerChange = instance . businessType === 'center_change' ? db . centerChangeRequests . find ( item => item . id === instance . businessId ) : null ;
const accountBatch = instance . businessType === 'candidate_account_batch' ? db . candidateAccountBatches . find ( item => item . id === instance . businessId ) : null ;
return {
... workflowView ( db , instance ) , candidateName : profile ? . name || '' , schoolName : profile ? . school || '' , className : profile ? . grade || '' ,
examName : registration ? db . exams . find ( item => item . id === registration . examId ) ? . name || '' : '' ,
centerName : centerChange ? . name || '' , requestType : centerChange ? . requestType || '' , centerChange : centerChange ? centerChangeView ( db , centerChange ) : null ,
accountBatch : accountBatch ? candidateAccountBatchView ( db , accountBatch ) : null ,
batchTotalCount : accountBatch ? db . candidateAccountBatchItems . filter ( item => item . batchId === accountBatch . id ) . length : 0
} ;
} ) ;
const availableAdmins = db . users . filter ( item => item . role === 'admin' && item . active ) . map ( safeUser ) ;
return sendJson ( response , 200 , { ok : true , instances , availableAdmins , canSupervise : user . adminLevel === 'super' } ) ;
}
const transferMatch = pathname . match ( /^\/api\/admin\/workflow-instances\/([^/]+)\/transfer$/ ) ;
if ( transferMatch && request . method === 'PATCH' ) {
const body = await readJson ( request ) ;
const instance = db . workflowInstances . find ( item => item . id === transferMatch [ 1 ] && item . status === 'pending' ) ;
if ( ! instance ) return sendError ( response , 404 , '待处理流程不存在' ) ;
const workflow = db . workflows . find ( item => item . id === instance . workflowId ) ;
const step = workflow ? . steps . find ( item => item . position === instance . currentStep ) ;
if ( user . adminLevel !== 'super' && instance . assigneeId !== user . id ) return sendError ( response , 403 , '只有当前处理人可以转交该流程' ) ;
const target = db . users . find ( item => item . id === body . assigneeId && item . role === 'admin' && item . active && item . adminLevel === step ? . adminLevel ) ;
if ( ! target ) return sendError ( response , 400 , '只能转交给当前步骤同级管理员' ) ;
const profile = workflowScopeProfile ( db , instance ) ;
if ( step . adminLevel === 'school' && target . schoolId !== profile ? . schoolId ) return sendError ( response , 400 , '校级流程只能转交给本校同级管理员' ) ;
const previous = instance . assigneeId ;
instance . assigneeId = target . id ;
const action = { id : uid ( 'flow_action' ) , instanceId : instance . id , actorId : user . id , action : 'transfer' , note : cleanText ( body . note , 300 ) , fromAssigneeId : previous , toAssigneeId : target . id , createdAt : nowIso ( ) } ;
const log = logAction ( db , user , '转交审批流程' , ` ${ workflow . name } → ${ target . displayName } ` ) ;
await database . transferWorkflow ( instance , action , log ) ;
return sendJson ( response , 200 , { ok : true , workflow : workflowView ( { ... db , workflowActions : [ ... db . workflowActions , action ] } , instance ) } ) ;
}
const superviseMatch = pathname . match ( /^\/api\/admin\/workflow-instances\/([^/]+)\/supervise$/ ) ;
if ( superviseMatch && request . method === 'PATCH' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const instance = db . workflowInstances . find ( item => item . id === superviseMatch [ 1 ] ) ;
if ( ! instance ) return sendError ( response , 404 , '流程不存在' ) ;
if ( instance . businessType === 'candidate_account_batch' && db . candidateAccountBatchItems . some ( item => item . batchId === instance . businessId && item . userId ) ) return sendError ( response , 409 , '已生成账号的批次不可重新打开,避免重复建号' ) ;
const workflow = db . workflows . find ( item => item . id === instance . workflowId ) ;
const requestedStep = Math . min ( workflow . steps . length , Math . max ( 1 , Number ( body . currentStep || instance . currentStep ) ) ) ;
const step = workflow . steps . find ( item => item . position === requestedStep ) ;
const profile = workflowScopeProfile ( db , instance ) ;
const eligible = adminsForStep ( db , step . adminLevel , profile ) ;
const assignee = eligible . find ( item => item . id === body . assigneeId ) || eligible [ 0 ] ;
if ( ! assignee ) return sendError ( response , 409 , '目标步骤没有可用管理员' ) ;
const previous = instance . assigneeId ;
const previousStep = instance . currentStep ;
instance . status = 'pending' ; instance . completedAt = null ; instance . currentStep = requestedStep ; instance . assigneeId = assignee . id ;
const note = cleanText ( body . note , 300 ) || ` 超级管理员将流程调整到第 ${ requestedStep } 步 ` ;
const action = { id : uid ( 'flow_action' ) , instanceId : instance . id , actorId : user . id , action : requestedStep < previousStep ? 'return' : 'supervise' , note , fromAssigneeId : previous , toAssigneeId : assignee . id , createdAt : nowIso ( ) } ;
const business = instance . businessType === 'profile_change'
? profile
: instance . businessType === 'registration_review'
? db . registrations . find ( item => item . id === instance . businessId )
: instance . businessType === 'center_change'
? db . centerChangeRequests . find ( item => item . id === instance . businessId )
: db . candidateAccountBatches . find ( item => item . id === instance . businessId ) ;
business . status = 'pending' ; business . reviewNote = note ; business . reviewedAt = null ; business . reviewerId = null ;
const log = logAction ( db , user , '监督调整审批流程' , ` ${ workflow . name } · 第 ${ requestedStep } 步 · ${ assignee . displayName } ` ) ;
await database . processWorkflow ( instance , action , business , log ) ;
return sendJson ( response , 200 , { ok : true , workflow : workflowView ( { ... db , workflowActions : [ ... db . workflowActions , action ] } , instance ) } ) ;
}
if ( request . method === 'GET' && pathname === '/api/admin/dashboard' ) {
const profiles = db . candidateProfiles . filter ( item => profileInScope ( user , item ) ) ;
const registrations = db . registrations . filter ( item => registrationInScope ( db , user , item ) ) ;
const visibleFlows = db . workflowInstances . filter ( instance => {
if ( user . adminLevel === 'super' ) return true ;
if ( user . adminLevel === 'class' ) return false ;
const business = workflowScopeProfile ( db , instance ) ;
return business && profileInScope ( user , business ) && ( instance . assigneeId === user . id || instance . status !== 'pending' ) ;
} ) ;
const pendingCandidates = profiles . filter ( item => item . status === 'pending' ) . length ;
const pendingRegistrations = registrations . filter ( item => item . status === 'pending' ) . length ;
return sendJson ( response , 200 , {
ok : true ,
admin : safeUser ( user ) ,
scopeLabel : adminScopeLabel ( db , user ) ,
permissions : permissionsByLevel [ user . adminLevel || 'super' ] ,
metrics : { candidates : profiles . length , pendingCandidates , registrations : registrations . length , pendingRegistrations , pendingFlows : visibleFlows . filter ( item => item . status === 'pending' ) . length , publishedExams : db . exams . filter ( item => item . status === 'published' ) . length , notices : db . notices . filter ( item => item . status === 'published' ) . length } ,
logs : user . adminLevel === 'super' ? db . auditLogs . slice ( 0 , 8 ) : db . auditLogs . filter ( log => log . actorId === user . id ) . slice ( 0 , 8 )
} ) ;
}
if ( request . method === 'GET' && pathname === '/api/admin/candidates' ) {
if ( ! requirePermission ( user , response , 'candidates.read' ) ) return true ;
const candidates = db . candidateProfiles . filter ( profile => profileInScope ( user , profile ) ) . map ( profile => {
const instance = pendingWorkflow ( db , 'profile_change' , profile . id ) || db . workflowInstances . filter ( item => item . businessType === 'profile_change' && item . businessId === profile . id ) [ 0 ] ;
const account = db . users . find ( item => item . id === profile . userId ) ;
return { ... profile , idNumberMasked : profile . idNumber . startsWith ( 'PENDING-' ) ? '待考生补充' : maskId ( profile . idNumber ) , username : account ? . username , candidateNumber : account ? . candidateNumber || '' , mustChangePassword : Boolean ( account ? . mustChangePassword ) , workflow : workflowView ( db , instance ) } ;
} ) ;
return sendJson ( response , 200 , { ok : true , candidates , schools : user . adminLevel === 'super' ? db . schools . filter ( item => item . active ) : db . schools . filter ( item => item . id === user . schoolId && item . active ) , classes : db . classes . filter ( item => item . active && ( user . adminLevel === 'super' || item . schoolId === user . schoolId ) ) } ) ;
}
const candidateMatch = pathname . match ( /^\/api\/admin\/candidates\/([^/]+)$/ ) ;
if ( request . method === 'PATCH' && candidateMatch ) {
if ( ! requirePermission ( user , response , 'candidates.review' ) ) return true ;
const body = await readJson ( request ) ;
const profile = db . candidateProfiles . find ( item => item . id === candidateMatch [ 1 ] ) ;
if ( ! profile ) return sendError ( response , 404 , '考生资料不存在' ) ;
if ( ! profileInScope ( user , profile ) && user . adminLevel !== 'super' ) return sendError ( response , 403 , '该考生不在你的数据范围内' ) ;
if ( ! [ 'approved' , 'rejected' ] . includes ( body . status ) ) return sendError ( response , 400 , '审核状态无效' ) ;
const instance = pendingWorkflow ( db , 'profile_change' , profile . id ) ;
if ( ! instance ) return sendError ( response , 409 , '当前没有待处理的考生信息流程' ) ;
const workflow = db . workflows . find ( item => item . id === instance . workflowId ) ;
const step = workflow ? . steps . find ( item => item . position === instance . currentStep ) ;
if ( user . adminLevel !== 'super' && ( instance . assigneeId !== user . id || step ? . adminLevel !== user . adminLevel ) ) return sendError ( response , 403 , '该流程当前未分配给你,可由当前处理人转交' ) ;
const note = cleanText ( body . reviewNote , 300 ) ;
const action = { id : uid ( 'flow_action' ) , instanceId : instance . id , actorId : user . id , action : body . status === 'approved' ? 'approve' : 'reject' , note , fromAssigneeId : instance . assigneeId , toAssigneeId : null , createdAt : nowIso ( ) } ;
if ( body . status === 'rejected' ) {
instance . status = 'rejected' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
profile . status = 'rejected' ; profile . reviewNote = note ; profile . reviewedAt = nowIso ( ) ; profile . reviewerId = user . id ;
} else if ( instance . currentStep < workflow . steps . length ) {
const nextStep = workflow . steps . find ( item => item . position === instance . currentStep + 1 ) ;
const nextAssignee = adminsForStep ( db , nextStep . adminLevel , profile ) [ 0 ] ;
if ( ! nextAssignee ) return sendError ( response , 409 , ` 没有可承接“ ${ nextStep . name } ”的管理员 ` ) ;
instance . currentStep += 1 ; instance . assigneeId = nextAssignee . id ; action . toAssigneeId = nextAssignee . id ;
profile . status = 'pending' ; profile . reviewNote = note ;
} else {
instance . status = 'approved' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
profile . status = 'approved' ; profile . reviewNote = note ; profile . reviewedAt = nowIso ( ) ; profile . reviewerId = user . id ;
}
const log = logAction ( db , user , body . status === 'approved' ? '处理考生信息流程' : '退回考生信息' , ` ${ profile . name } : ${ note || '无备注' } ` ) ;
await database . processWorkflow ( instance , action , profile , log ) ;
return sendJson ( response , 200 , { ok : true , profile , workflow : workflowView ( { ... db , workflowActions : [ ... db . workflowActions , action ] } , instance ) } ) ;
}
if ( request . method === 'GET' && pathname === '/api/admin/registrations' ) {
if ( ! requirePermission ( user , response , 'registrations.read' ) ) return true ;
const registrations = db . registrations . filter ( registration => registrationInScope ( db , user , registration ) ) . map ( registration => {
const profile = db . candidateProfiles . find ( item => item . userId === registration . userId ) ;
return { ... examRegistrationView ( db , registration ) , candidate : profile ? { ... profile , idNumber : maskId ( profile . idNumber ) } : null } ;
} ) ;
return sendJson ( response , 200 , { ok : true , registrations } ) ;
}
const registrationMatch = pathname . match ( /^\/api\/admin\/registrations\/([^/]+)$/ ) ;
if ( request . method === 'PATCH' && registrationMatch ) {
if ( ! requirePermission ( user , response , 'registrations.review' ) ) return true ;
const body = await readJson ( request ) ;
const registration = db . registrations . find ( item => item . id === registrationMatch [ 1 ] ) ;
if ( ! registration ) return sendError ( response , 404 , '报名记录不存在' ) ;
if ( ! registrationInScope ( db , user , registration ) && user . adminLevel !== 'super' ) return sendError ( response , 403 , '该报名不在你的数据范围内' ) ;
if ( ! [ 'approved' , 'rejected' ] . includes ( body . status ) ) return sendError ( response , 400 , '审核状态无效' ) ;
const profile = db . candidateProfiles . find ( item => item . userId === registration . userId ) ;
const instance = pendingWorkflow ( db , 'registration_review' , registration . id ) ;
if ( ! instance ) return sendError ( response , 409 , '当前没有待处理的报名审核流程' ) ;
const workflow = db . workflows . find ( item => item . id === instance . workflowId ) ;
const step = workflow ? . steps . find ( item => item . position === instance . currentStep ) ;
if ( user . adminLevel !== 'super' && ( instance . assigneeId !== user . id || step ? . adminLevel !== user . adminLevel ) ) return sendError ( response , 403 , '该流程当前未分配给你,可由当前处理人转交' ) ;
const note = cleanText ( body . reviewNote , 300 ) ;
const action = { id : uid ( 'flow_action' ) , instanceId : instance . id , actorId : user . id , action : body . status === 'approved' ? 'approve' : 'reject' , note , fromAssigneeId : instance . assigneeId , toAssigneeId : null , createdAt : nowIso ( ) } ;
if ( body . status === 'rejected' ) {
instance . status = 'rejected' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
registration . status = 'rejected' ; registration . reviewNote = note ; registration . reviewedAt = nowIso ( ) ;
} else if ( instance . currentStep < workflow . steps . length ) {
const nextStep = workflow . steps . find ( item => item . position === instance . currentStep + 1 ) ;
const nextAssignee = adminsForStep ( db , nextStep . adminLevel , profile ) [ 0 ] ;
if ( ! nextAssignee ) return sendError ( response , 409 , ` 没有可承接“ ${ nextStep . name } ”的管理员 ` ) ;
instance . currentStep += 1 ; instance . assigneeId = nextAssignee . id ; action . toAssigneeId = nextAssignee . id ;
registration . status = 'pending' ; registration . reviewNote = note ;
} else {
const account = db . users . find ( item => item . id === registration . userId ) ;
if ( ! account ? . candidateNumber ) return sendError ( response , 409 , '考生账户尚未分配报名号,请先在报名号管理中完成分配' ) ;
instance . status = 'approved' ; instance . completedAt = nowIso ( ) ; instance . assigneeId = null ;
registration . status = 'approved' ; registration . paymentStatus = 'paid' ; registration . reviewNote = note ; registration . reviewedAt = nowIso ( ) ;
registration . registrationNumber = account . candidateNumber ;
registration . numberRuleId = db . numberRules . find ( item => item . active ) ? . id || registration . numberRuleId ;
}
const log = logAction ( db , user , body . status === 'approved' ? '处理报名审核流程' : '退回考试报名' , ` ${ profile ? . name || registration . userId } · ${ db . exams . find ( item => item . id === registration . examId ) ? . name } ` ) ;
await database . processWorkflow ( instance , action , registration , log ) ;
return sendJson ( response , 200 , { ok : true , registration , workflow : workflowView ( { ... db , workflowActions : [ ... db . workflowActions , action ] } , instance ) } ) ;
}
const admitMatch = pathname . match ( /^\/api\/admin\/registrations\/([^/]+)\/admit-card$/ ) ;
if ( request . method === 'POST' && admitMatch ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const registration = db . registrations . find ( item => item . id === admitMatch [ 1 ] ) ;
if ( ! registration ) return sendError ( response , 404 , '报名记录不存在' ) ;
if ( registration . status !== 'approved' ) return sendError ( response , 400 , '报名审核通过后才能生成准考证' ) ;
if ( ! registration . admitCard ) {
const exam = db . exams . find ( item => item . id === registration . examId ) ;
const sequence = String ( db . registrations . filter ( item => item . examId === exam . id && item . admitCard ) . length + 1 ) . padStart ( 4 , '0' ) ;
registration . admitCard = {
number : ` ${ exam . code . replace ( /[^a-z0-9]/gi , '' ) . toUpperCase ( ) . slice ( - 12 ) || String ( new Date ( ) . getFullYear ( ) ) } - ${ sequence } ` ,
testCenter : cleanText ( ( await readJson ( request ) ) . testCenter || '海州市第一中学' , 80 ) ,
room : ` 0 ${ Math . ceil ( Number ( sequence ) / 30 ) || 1 } 考场 ` ,
seat : String ( ( ( Number ( sequence ) - 1 ) % 30 ) + 1 ) . padStart ( 2 , '0' ) ,
generatedAt : nowIso ( )
} ;
const profile = db . candidateProfiles . find ( item => item . userId === registration . userId ) ;
const log = logAction ( db , user , '生成准考证' , ` ${ profile ? . name || registration . userId } · ${ registration . admitCard . number } ` ) ;
await database . createAdmitCard ( registration . id , registration . admitCard , log ) ;
}
return sendJson ( response , 200 , { ok : true , admitCard : registration . admitCard } ) ;
}
if ( request . method === 'GET' && pathname === '/api/admin/exams' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
return sendJson ( response , 200 , { ok : true , exams : db . exams . map ( exam => ( { ... publicExam ( exam ) , registrationCount : db . registrations . filter ( reg => reg . examId === exam . id ) . length } ) ) } ) ;
}
if ( request . method === 'POST' && pathname === '/api/admin/exams' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const name = cleanText ( body . name , 100 ) ;
if ( ! name || ! body . registrationStart || ! body . registrationEnd || ! body . examStart || ! body . examEnd ) return sendError ( response , 400 , '请完整填写考试名称和关键日期' ) ;
const subjectNames = Array . isArray ( body . subjects ) ? body . subjects : String ( body . subjects || '' ) . split ( /[,, ]/ ) ;
const subjects = subjectNames . map ( name => cleanText ( typeof name === 'string' ? name : name . name , 30 ) ) . filter ( Boolean ) . map ( ( name , index ) => ( { id : uid ( 'sub' ) , name , date : cleanText ( body . examStart , 10 ) , start : '09:00' , end : '11:00' , fee : 0 , order : index + 1 } ) ) ;
if ( ! subjects . length ) return sendError ( response , 400 , '请至少添加一个考试科目' ) ;
const exam = { id : uid ( 'exam' ) , code : cleanText ( body . code , 30 ) || ` EX- ${ new Date ( ) . getFullYear ( ) } - ${ String ( db . exams . length + 1 ) . padStart ( 2 , '0' ) } ` , name , description : cleanText ( body . description , 500 ) , registrationStart : body . registrationStart , registrationEnd : body . registrationEnd , examStart : body . examStart , examEnd : body . examEnd , admitDownloadStart : body . admitDownloadStart || body . registrationEnd , admitDownloadEnd : body . admitDownloadEnd || body . examStart , location : cleanText ( body . location , 100 ) , status : body . status === 'published' ? 'published' : 'draft' , subjects , createdAt : nowIso ( ) } ;
const log = logAction ( db , user , '创建考试' , ` ${ exam . name } · ${ subjects . length } 个科目 ` ) ;
await database . createExam ( exam , log ) ;
return sendJson ( response , 201 , { ok : true , exam } ) ;
}
const examMatch = pathname . match ( /^\/api\/admin\/exams\/([^/]+)$/ ) ;
if ( request . method === 'PATCH' && examMatch ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const exam = db . exams . find ( item => item . id === examMatch [ 1 ] ) ;
if ( ! exam ) return sendError ( response , 404 , '考试不存在' ) ;
const originalStatus = exam . status ;
const detailFields = [ 'code' , 'name' , 'description' , 'location' , 'registrationStart' , 'registrationEnd' , 'examStart' , 'examEnd' , 'admitDownloadStart' , 'admitDownloadEnd' ] ;
const editingDetails = detailFields . some ( field => body [ field ] != null ) || body . subjects != null ;
if ( editingDetails && originalStatus !== 'draft' ) return sendError ( response , 409 , '请先将考试撤回为草稿后再编辑' ) ;
if ( body . status && [ 'draft' , 'published' , 'closed' ] . includes ( body . status ) ) exam . status = body . status ;
detailFields . forEach ( field => { if ( body [ field ] != null ) exam [ field ] = cleanText ( body [ field ] , field === 'description' ? 500 : 100 ) ; } ) ;
let replaceSubjects = false ;
if ( body . subjects != null ) {
if ( db . registrations . some ( registration => registration . examId === exam . id ) ) return sendError ( response , 409 , '已有报名记录,不能修改考试科目' ) ;
const subjectNames = Array . isArray ( body . subjects ) ? body . subjects : String ( body . subjects || '' ) . split ( /[,, ]/ ) ;
const names = subjectNames . map ( item => cleanText ( typeof item === 'string' ? item : item . name , 30 ) ) . filter ( Boolean ) ;
if ( ! names . length ) return sendError ( response , 400 , '请至少添加一个考试科目' ) ;
exam . subjects = names . map ( ( name , index ) => ( { id : uid ( 'sub' ) , name , date : String ( exam . examStart ) . slice ( 0 , 10 ) , start : '09:00' , end : '11:00' , fee : 0 , order : index + 1 } ) ) ;
replaceSubjects = true ;
}
if ( ! exam . name || ! exam . registrationStart || ! exam . registrationEnd || ! exam . examStart || ! exam . examEnd ) return sendError ( response , 400 , '请完整填写考试名称和关键日期' ) ;
if ( exam . status === 'published' && ! exam . subjects . length ) return sendError ( response , 400 , '请先配置考试科目再发布' ) ;
const log = logAction ( db , user , '更新考试' , ` ${ exam . name } · 状态 ${ exam . status } ` ) ;
await database . updateExam ( exam , log , replaceSubjects ) ;
return sendJson ( response , 200 , { ok : true , exam } ) ;
}
if ( request . method === 'GET' && pathname === '/api/admin/notices' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
return sendJson ( response , 200 , { ok : true , notices : db . notices . sort ( ( a , b ) => new Date ( b . publishAt || b . createdAt ) - new Date ( a . publishAt || a . createdAt ) ) } ) ;
}
if ( request . method === 'POST' && pathname === '/api/admin/notices' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const title = cleanText ( body . title , 120 ) ;
const content = cleanText ( body . content , 5000 ) ;
if ( ! title || ! content ) return sendError ( response , 400 , '通知标题和正文不能为空' ) ;
const notice = { id : uid ( 'notice' ) , title , summary : cleanText ( body . summary , 260 ) || content . slice ( 0 , 80 ) , content , category : cleanText ( body . category , 30 ) || '通知公告' , pinned : Boolean ( body . pinned ) , status : body . status === 'draft' ? 'draft' : 'published' , publishAt : body . status === 'draft' ? null : nowIso ( ) , createdAt : nowIso ( ) , author : user . displayName } ;
const log = logAction ( db , user , notice . status === 'published' ? '发布通知' : '保存通知草稿' , notice . title ) ;
await database . createNotice ( notice , log ) ;
return sendJson ( response , 201 , { ok : true , notice } ) ;
}
const noticeMatch = pathname . match ( /^\/api\/admin\/notices\/([^/]+)$/ ) ;
if ( request . method === 'PATCH' && noticeMatch ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const notice = db . notices . find ( item => item . id === noticeMatch [ 1 ] ) ;
if ( ! notice ) return sendError ( response , 404 , '通知不存在' ) ;
[ 'title' , 'summary' , 'content' , 'category' ] . forEach ( field => { if ( body [ field ] != null ) notice [ field ] = cleanText ( body [ field ] , field === 'content' ? 5000 : 260 ) ; } ) ;
if ( body . pinned != null ) notice . pinned = Boolean ( body . pinned ) ;
if ( body . status && [ 'draft' , 'published' ] . includes ( body . status ) ) {
notice . status = body . status ;
if ( body . status === 'published' && ! notice . publishAt ) notice . publishAt = nowIso ( ) ;
}
const log = logAction ( db , user , '更新通知' , ` ${ notice . title } · ${ notice . status } ` ) ;
await database . updateNotice ( notice , log ) ;
return sendJson ( response , 200 , { ok : true , notice } ) ;
}
if ( request . method === 'GET' && pathname === '/api/admin/results' ) {
if ( ! requirePermission ( user , response , 'results.read' ) ) return true ;
const scopedRegistrations = db . registrations . filter ( item => registrationInScope ( db , user , item ) ) ;
const results = db . results . filter ( result => scopedRegistrations . some ( item => item . id === result . registrationId ) ) . map ( result => {
const registration = db . registrations . find ( item => item . id === result . registrationId ) ;
const profile = db . candidateProfiles . find ( item => item . userId === registration ? . userId ) ;
const exam = db . exams . find ( item => item . id === registration ? . examId ) ;
const subject = exam ? . subjects . find ( item => item . id === result . subjectId ) ;
return { ... result , candidateName : profile ? . name , examName : exam ? . name , subjectName : subject ? . name } ;
} ) ;
return sendJson ( response , 200 , { ok : true , results , registrations : user . adminLevel === 'super' ? scopedRegistrations . filter ( item => item . status === 'approved' ) . map ( item => examRegistrationView ( db , item ) ) : [ ] } ) ;
}
if ( request . method === 'POST' && pathname === '/api/admin/results' ) {
if ( ! requirePermission ( user , response , '*' ) ) return true ;
const body = await readJson ( request ) ;
const registration = db . registrations . find ( item => item . id === body . registrationId && item . status === 'approved' ) ;
if ( ! registration ) return sendError ( response , 404 , '已通过的报名记录不存在' ) ;
const exam = db . exams . find ( item => item . id === registration . examId ) ;
if ( ! registration . subjectIds . includes ( body . subjectId ) || ! exam . subjects . some ( item => item . id === body . subjectId ) ) return sendError ( response , 400 , '该考生未报名此科目' ) ;
const score = Number ( body . score ) ;
if ( ! Number . isFinite ( score ) || score < 0 || score > 150 ) return sendError ( response , 400 , '成绩必须在 0—150 之间' ) ;
let result = db . results . find ( item => item . registrationId === registration . id && item . subjectId === body . subjectId ) ;
const isNew = ! result ;
if ( ! result ) {
result = { id : uid ( 'result' ) , registrationId : registration . id , subjectId : body . subjectId } ;
db . results . push ( result ) ;
}
Object . assign ( result , { score , grade : cleanText ( body . grade , 10 ) || ( score >= 135 ? 'A+' : score >= 120 ? 'A' : score >= 105 ? 'B+' : score >= 90 ? 'B' : score >= 60 ? 'C' : 'D' ) , published : Boolean ( body . published ) , updatedAt : nowIso ( ) , publishedAt : body . published ? nowIso ( ) : null } ) ;
const profile = db . candidateProfiles . find ( item => item . userId === registration . userId ) ;
const subject = exam . subjects . find ( item => item . id === body . subjectId ) ;
const log = logAction ( db , user , body . published ? '发布成绩' : '保存成绩' , ` ${ profile ? . name } · ${ subject ? . name } · ${ score } ` ) ;
await database . saveResult ( result , isNew , log ) ;
return sendJson ( response , 200 , { ok : true , result } ) ;
}
return sendError ( response , 404 , '管理功能接口不存在' ) ;
}
return handleAdmin ;
}