流程设计
This commit is contained in:
+64
-13
@@ -16,6 +16,7 @@ export function createAdminRoutes(context) {
|
||||
registrationInScope,
|
||||
adminScopeLabel,
|
||||
adminsForStep,
|
||||
selectAdminForStep,
|
||||
activeWorkflow,
|
||||
createWorkflowSubmission,
|
||||
workflowView,
|
||||
@@ -267,7 +268,7 @@ export function createAdminRoutes(context) {
|
||||
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];
|
||||
const nextAssignee = selectAdminForStep(db, nextStep.adminLevel, centerScopeProfile(db, batch.schoolId));
|
||||
if (!nextAssignee) return sendError(response, 409, `没有可承接“${nextStep.name}”的管理员`);
|
||||
instance.currentStep += 1; instance.assigneeId = nextAssignee.id; action.toAssigneeId = nextAssignee.id;
|
||||
batch.reviewNote = note;
|
||||
@@ -362,7 +363,7 @@ export function createAdminRoutes(context) {
|
||||
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];
|
||||
const nextAssignee = selectAdminForStep(db, nextStep.adminLevel, centerScopeProfile(db, change.schoolId));
|
||||
if (!nextAssignee) return sendError(response, 409, `没有可承接“${nextStep.name}”的管理员`);
|
||||
instance.currentStep += 1; instance.assigneeId = nextAssignee.id; action.toAssigneeId = nextAssignee.id;
|
||||
change.reviewNote = note;
|
||||
@@ -417,14 +418,15 @@ export function createAdminRoutes(context) {
|
||||
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)$/);
|
||||
const workflowDefinitionMatch = pathname.match(/^\/api\/admin\/workflows\/(profile_change|registration_review|center_change|candidate_account_batch|score_appeal)$/);
|
||||
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 (!steps.length || steps.some(item => !['class', 'school', 'super'].includes(item.adminLevel))) return sendError(response, 400, '流程至少需要一个班级、校级或超级管理员审批步骤');
|
||||
if (['center_change', 'candidate_account_batch'].includes(workflowDefinitionMatch[1]) && steps.some(item => item.adminLevel === 'class')) 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;
|
||||
@@ -436,7 +438,7 @@ export function createAdminRoutes(context) {
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/workflow-instances' && request.method === 'GET') {
|
||||
if (user.adminLevel === 'class') return sendError(response, 403, '班级管理员只读查看考生、成绩和报名状态');
|
||||
if (!requirePermission(user, response, 'workflows.inbox')) return true;
|
||||
const instances = db.workflowInstances.filter(instance => {
|
||||
if (user.adminLevel === 'super') return true;
|
||||
const profile = workflowScopeProfile(db, instance);
|
||||
@@ -446,12 +448,17 @@ export function createAdminRoutes(context) {
|
||||
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;
|
||||
const appealResult = instance.businessType === 'score_appeal' ? db.results.find(item => item.id === instance.businessId) : null;
|
||||
const appealRegistration = appealResult ? db.registrations.find(item => item.id === appealResult.registrationId) : null;
|
||||
const appealExam = appealRegistration ? db.exams.find(item => item.id === appealRegistration.examId) : null;
|
||||
const appealSubject = appealExam?.subjects.find(item => item.id === appealResult?.subjectId);
|
||||
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
|
||||
batchTotalCount: accountBatch ? db.candidateAccountBatchItems.filter(item => item.batchId === accountBatch.id).length : 0,
|
||||
appealResult: appealResult ? { score: appealResult.score, grade: appealResult.grade, examName: appealExam?.name || '', subjectName: appealSubject?.name || '' } : null
|
||||
};
|
||||
});
|
||||
const availableAdmins = db.users.filter(item => item.role === 'admin' && item.active).map(safeUser);
|
||||
@@ -469,6 +476,7 @@ export function createAdminRoutes(context) {
|
||||
if (!target) return sendError(response, 400, '只能转交给当前步骤同级管理员');
|
||||
const profile = workflowScopeProfile(db, instance);
|
||||
if (step.adminLevel === 'school' && target.schoolId !== profile?.schoolId) return sendError(response, 400, '校级流程只能转交给本校同级管理员');
|
||||
if (step.adminLevel === 'class' && (target.schoolId !== profile?.schoolId || target.classId !== profile?.classId)) 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() };
|
||||
@@ -488,7 +496,9 @@ export function createAdminRoutes(context) {
|
||||
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];
|
||||
const requestedAssignee = body.assigneeId ? eligible.find(item => item.id === body.assigneeId) : null;
|
||||
if (body.assigneeId && !requestedAssignee) return sendError(response, 400, '指定管理员不在该学校或班级的目标步骤范围内');
|
||||
const assignee = requestedAssignee || selectAdminForStep(db, step.adminLevel, profile);
|
||||
if (!assignee) return sendError(response, 409, '目标步骤没有可用管理员');
|
||||
const previous = instance.assigneeId;
|
||||
const previousStep = instance.currentStep;
|
||||
@@ -501,8 +511,12 @@ export function createAdminRoutes(context) {
|
||||
? 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;
|
||||
: instance.businessType === 'candidate_account_batch'
|
||||
? db.candidateAccountBatches.find(item => item.id === instance.businessId)
|
||||
: null;
|
||||
if (business) {
|
||||
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) });
|
||||
@@ -513,7 +527,6 @@ export function createAdminRoutes(context) {
|
||||
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');
|
||||
});
|
||||
@@ -557,7 +570,7 @@ export function createAdminRoutes(context) {
|
||||
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];
|
||||
const nextAssignee = selectAdminForStep(db, nextStep.adminLevel, profile);
|
||||
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;
|
||||
@@ -598,7 +611,7 @@ export function createAdminRoutes(context) {
|
||||
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];
|
||||
const nextAssignee = selectAdminForStep(db, nextStep.adminLevel, profile);
|
||||
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;
|
||||
@@ -615,6 +628,39 @@ export function createAdminRoutes(context) {
|
||||
return sendJson(response, 200, { ok: true, registration, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) });
|
||||
}
|
||||
const admitMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)\/admit-card$/);
|
||||
const scoreAppealMatch = pathname.match(/^\/api\/admin\/score-appeals\/([^/]+)$/);
|
||||
if (request.method === 'PATCH' && scoreAppealMatch) {
|
||||
if (!requirePermission(user, response, 'workflows.inbox')) return true;
|
||||
const body = await readJson(request);
|
||||
if (!['approved', 'rejected'].includes(body.status)) return sendError(response, 400, '复议处理状态无效');
|
||||
const result = db.results.find(item => item.id === scoreAppealMatch[1] && item.published);
|
||||
const registration = db.registrations.find(item => item.id === result?.registrationId);
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration?.userId);
|
||||
if (!result || !registration || !profile) return sendError(response, 404, '待处理的成绩复议不存在');
|
||||
if (!profileInScope(user, profile)) return sendError(response, 403, '该成绩复议不在你的数据范围内');
|
||||
const instance = pendingWorkflow(db, 'score_appeal', result.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() };
|
||||
if (body.status === 'rejected') {
|
||||
instance.status = 'rejected'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
} else if (instance.currentStep < workflow.steps.length) {
|
||||
const nextStep = workflow.steps.find(item => item.position === instance.currentStep + 1);
|
||||
const nextAssignee = selectAdminForStep(db, nextStep.adminLevel, profile);
|
||||
if (!nextAssignee) return sendError(response, 409, `没有可承接“${nextStep.name}”的管理员`);
|
||||
instance.currentStep += 1; instance.assigneeId = nextAssignee.id; action.toAssigneeId = nextAssignee.id;
|
||||
} else {
|
||||
instance.status = 'approved'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
}
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const subject = exam?.subjects.find(item => item.id === result.subjectId);
|
||||
const log = logAction(db, user, body.status === 'approved' ? '处理成绩复议' : '退回成绩复议', `${profile.name} · ${exam?.name || ''} · ${subject?.name || ''}`);
|
||||
await database.processWorkflow(instance, action, null, log);
|
||||
return sendJson(response, 200, { ok: true, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) });
|
||||
}
|
||||
if (request.method === 'POST' && admitMatch) {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const registration = db.registrations.find(item => item.id === admitMatch[1]);
|
||||
@@ -724,7 +770,12 @@ export function createAdminRoutes(context) {
|
||||
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)) : [] });
|
||||
const appeals = db.workflowInstances.filter(instance => instance.businessType === 'score_appeal' && results.some(result => result.id === instance.businessId)).map(instance => {
|
||||
const result = results.find(item => item.id === instance.businessId);
|
||||
const workflow = workflowView(db, instance);
|
||||
return { ...workflow, result, reason: workflow.actions.find(action => action.action === 'submit')?.note || '' };
|
||||
});
|
||||
return sendJson(response, 200, { ok: true, results, appeals, 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;
|
||||
|
||||
Reference in New Issue
Block a user