taskId = $taskId; // 复用现有 pdf 队列,与历史部署/消费者一致 $this->onQueue('pdf'); $this->afterCommit(); } public function handle( LearningAnalyticsService $learningAnalyticsService, QuestionBankService $questionBankService, WrongQuestionPracticePlanService $wrongQuestionPracticePlanService, TaskManager $taskManager ): void { $task = $taskManager->getTaskStatus($this->taskId); if (!is_array($task) || empty($task['data']) || !is_array($task['data'])) { $taskManager->markTaskFailed($this->taskId, '任务数据不存在'); return; } $data = $task['data']; $assembleStartedAt = microtime(true); $phaseStartedAt = $assembleStartedAt; try { $taskManager->updateTaskProgress($this->taskId, 5, '开始异步组卷...'); $requestedAssembleType = (int) ($data['assemble_type'] ?? 4); $strategyAssembleType = AssembleType::toStrategyType($requestedAssembleType); $difficultyCategory = $data['difficulty_category'] ?? 1; $paperName = $data['paper_name'] ?? ('智能试卷_'.now()->format('Ymd_His')); $mistakeIds = $data['mistake_ids'] ?? []; $mistakeQuestionIds = $data['mistake_question_ids'] ?? []; $paperIds = $data['paper_ids'] ?? []; $questionTypeRatio = $this->normalizeQuestionTypeRatio($data['question_type_ratio'] ?? []); $questions = []; $result = null; $diagnosticChapterId = null; $explanationKpCodes = null; $wrongQuestionPracticePlan = null; if (in_array($requestedAssembleType, [15, 16], true)) { // assemble_type=15(错题再练):paper_ids 为题库 question_id,直组原错题。 // assemble_type=16(错题追练):paper_ids 仍为题库 question_id,但只用来生成知识点组卷计划。 $questionIdList = $this->normalizeBankQuestionIdsList($paperIds); if ($questionIdList === []) { $taskManager->markTaskFailed($this->taskId, ($requestedAssembleType === 16 ? '错题追练' : '错题再练').'组卷需提供 paper_ids(题库题目 id)'); return; } if ($requestedAssembleType === 16) { $wrongQuestionPracticePlan = $wrongQuestionPracticePlanService->build( (string) $data['student_id'], $questionIdList, (int) ($data['total_questions'] ?? config('question_bank.default_total_questions')) ); if (empty($wrongQuestionPracticePlan['usable'])) { $taskManager->markTaskFailed($this->taskId, $wrongQuestionPracticePlan['message'] ?? '错题追练没有可用的知识点题目'); return; } $paperName = $data['paper_name'] ?? ('错题追练_'.$data['student_id'].'_'.now()->format('Ymd_His')); $params = [ 'student_id' => $data['student_id'], 'grade' => $data['grade'] ?? null, 'total_questions' => (int) ($wrongQuestionPracticePlan['target_questions'] ?? ($data['total_questions'] ?? config('question_bank.default_total_questions'))), 'kp_codes' => $wrongQuestionPracticePlan['kp_code_list'] ?? [], 'skills' => $data['skills'] ?? [], 'question_type_ratio' => $wrongQuestionPracticePlan['question_type_ratio'] ?? $questionTypeRatio, 'difficulty_category' => $difficultyCategory, 'assemble_type' => 2, 'exam_type' => 'knowledge', 'paper_ids' => [], 'textbook_id' => $data['textbook_id'] ?? null, 'end_catalog_id' => $data['end_catalog_id'] ?? null, 'chapter_id_list' => $data['chapter_id_list'] ?? null, 'kp_code_list' => $wrongQuestionPracticePlan['kp_code_list'] ?? [], 'kp_target_counts' => $wrongQuestionPracticePlan['kp_target_counts'] ?? [], 'target_difficulty_by_kp' => $wrongQuestionPracticePlan['target_difficulty_by_kp'] ?? [], 'max_difficulty_by_kp' => $wrongQuestionPracticePlan['max_difficulty_by_kp'] ?? [], 'type_targets_by_kp' => $wrongQuestionPracticePlan['type_targets_by_kp'] ?? [], 'exclude_question_ids' => $wrongQuestionPracticePlan['exclude_question_ids'] ?? [], 'wrong_question_practice_plan' => $wrongQuestionPracticePlan, ]; $result = $learningAnalyticsService->generateIntelligentExam($params); if (empty($result['success'])) { $taskManager->markTaskFailed($this->taskId, $result['message'] ?? '错题追练组卷未生成题目'); return; } if (isset($result['stats']['difficulty_category'])) { $difficultyCategory = $result['stats']['difficulty_category']; } $diagnosticChapterId = $result['diagnostic_chapter_id'] ?? null; $explanationKpCodes = $result['explanation_kp_codes'] ?? null; $result['assemble_type'] = 16; $questions = $this->hydrateQuestions($result['questions'] ?? [], $wrongQuestionPracticePlan['kp_code_list'] ?? []); if (empty($questions)) { $taskManager->markTaskFailed($this->taskId, '错题追练组卷未生成有效题目'); return; } } else { $strict = $this->resolveMistakeQuestionIdsStrictForStudent( (string) $data['student_id'], [], array_map(static fn ($id) => (string) $id, $questionIdList) ); if (! ($strict['ok'] ?? false)) { $taskManager->markTaskFailed($this->taskId, $strict['message'] ?? '错题校验失败'); return; } $questionIds = $strict['question_ids']; $bankQuestions = $questionBankService->getQuestionsByIds($questionIds)['data'] ?? []; if (empty($bankQuestions)) { $taskManager->markTaskFailed($this->taskId, '错题对应题库题目不可用'); return; } $questions = $this->hydrateQuestions($bankQuestions, $data['kp_codes'] ?? []); $questions = app(QuestionDifficultyResolver::class)->applyCalibratedDifficulty($questions); $questions = $this->sortQuestionsByRequestedIds($questions, $questionIds); $paperName = $data['paper_name'] ?? ('错题再练_'.$data['student_id'].'_'.now()->format('Ymd_His')); } } elseif (! empty($mistakeIds) || ! empty($mistakeQuestionIds)) { // assemble_type=5 时 mistake_ids / mistake_question_ids 须严格归属该学生;其它类型走宽松解析。 if ($requestedAssembleType === 5) { $strict = $this->resolveMistakeQuestionIdsStrictForStudent( (string) $data['student_id'], $mistakeIds, $mistakeQuestionIds ); if (! ($strict['ok'] ?? false)) { $taskManager->markTaskFailed($this->taskId, $strict['message'] ?? '错题校验失败'); return; } $questionIds = $strict['question_ids']; } else { $questionIds = $this->resolveMistakeQuestionIds((string) $data['student_id'], $mistakeIds, $mistakeQuestionIds); } if (empty($questionIds)) { $taskManager->markTaskFailed($this->taskId, '未找到可用的错题题目'); return; } $bankQuestions = $questionBankService->getQuestionsByIds($questionIds)['data'] ?? []; if (empty($bankQuestions)) { $taskManager->markTaskFailed($this->taskId, '错题对应题库题目不可用'); return; } $questions = $this->hydrateQuestions($bankQuestions, $data['kp_codes'] ?? []); $questions = app(QuestionDifficultyResolver::class)->applyCalibratedDifficulty($questions); $questions = $this->sortQuestionsByRequestedIds($questions, $questionIds); $paperName = $data['paper_name'] ?? ('错题复习_'.$data['student_id'].'_'.now()->format('Ymd_His')); } else { $params = [ 'student_id' => $data['student_id'], 'grade' => $data['grade'] ?? null, 'total_questions' => $data['total_questions'], 'kp_codes' => $strategyAssembleType === 3 ? null : ($data['kp_codes'] ?? null), 'skills' => $data['skills'] ?? [], 'question_type_ratio' => $questionTypeRatio, 'difficulty_category' => $difficultyCategory, 'assemble_type' => $strategyAssembleType, 'exam_type' => $data['exam_type'] ?? 'general', 'paper_ids' => $paperIds, 'textbook_id' => $data['textbook_id'] ?? null, 'end_catalog_id' => $data['end_catalog_id'] ?? null, 'chapter_id_list' => $data['chapter_id_list'] ?? null, 'kp_code_list' => $strategyAssembleType === 3 ? null : ($data['kp_code_list'] ?? $data['kp_codes'] ?? []), 'practice_options' => $data['practice_options'] ?? null, 'mistake_options' => $data['mistake_options'] ?? null, ]; $result = $learningAnalyticsService->generateIntelligentExam($params); if (empty($result['success'])) { $taskManager->markTaskFailed($this->taskId, $result['message'] ?? '智能出卷失败'); return; } if (isset($result['stats']['difficulty_category'])) { $difficultyCategory = $result['stats']['difficulty_category']; } $diagnosticChapterId = $result['diagnostic_chapter_id'] ?? null; $explanationKpCodes = $result['explanation_kp_codes'] ?? null; $questions = $this->hydrateQuestions($result['questions'] ?? [], $data['kp_codes'] ?? []); } Log::info('assemble.job.timing', [ 'task_id' => $this->taskId, 'phase' => 'select_and_prepare_questions', 'elapsed_ms' => (int) round((microtime(true) - $phaseStartedAt) * 1000), 'assemble_type' => $requestedAssembleType, 'strategy_assemble_type' => $strategyAssembleType, 'question_count' => count($questions), ]); if (empty($questions)) { $taskManager->markTaskFailed($this->taskId, '未能生成有效题目'); return; } $totalQuestions = min((int) ($data['total_questions'] ?? 10), count($questions)); $questions = array_slice($questions, 0, $totalQuestions); $questions = $this->sortQuestionsWithinTypeByDifficulty($questions); $targetTotalScore = (float) ($data['total_score'] ?? 100.0); $questions = $this->adjustQuestionScores($questions, $targetTotalScore); $totalScore = array_sum(array_column($questions, 'score')); $finalAssembleType = ($result !== null && isset($result['assemble_type'])) ? (int) $result['assemble_type'] : $requestedAssembleType; if (in_array($requestedAssembleType, [11, 12, 13], true)) { $finalAssembleType = $requestedAssembleType; } if ($finalAssembleType === 16) { $difficultyCategory = $this->deriveDifficultyCategoryFromSelectedDistribution($questions); } $requestPayloadParams = $data['request_payload_snapshot_raw'] ?? null; $phaseStartedAt = microtime(true); $paperId = $questionBankService->saveExamToDatabase([ 'paper_id' => $data['paper_id'] ?? null, 'paper_name' => $paperName, 'student_id' => $data['student_id'], 'teacher_id' => $data['teacher_id'] ?? null, 'params' => $requestPayloadParams, 'assembleType' => $finalAssembleType, 'difficulty_category' => $difficultyCategory, 'total_score' => $totalScore, 'questions' => $questions, 'diagnostic_chapter_id' => $diagnosticChapterId, 'explanation_kp_codes' => $explanationKpCodes, ]); if (! $paperId) { $taskManager->markTaskFailed($this->taskId, '试卷保存失败'); return; } Log::info('assemble.job.timing', [ 'task_id' => $this->taskId, 'phase' => 'save_exam_to_database', 'elapsed_ms' => (int) round((microtime(true) - $phaseStartedAt) * 1000), 'paper_id' => $paperId, ]); $finalStats = $result['stats'] ?? [ 'total_selected' => count($questions), 'mistake_based' => ! empty($mistakeIds) || ! empty($mistakeQuestionIds) || in_array($requestedAssembleType, [15, 16], true), ]; if ($wrongQuestionPracticePlan !== null) { $finalStats['wrong_question_practice_plan'] = $wrongQuestionPracticePlan; } if (! isset($finalStats['difficulty_category'])) { $finalStats['difficulty_category'] = $difficultyCategory; } if ($finalAssembleType === 16) { $finalStats['difficulty_category'] = $difficultyCategory; $finalStats['final_avg_difficulty'] = $this->averageQuestionDifficulty($questions); } $taskManager->updateTaskStatus($this->taskId, [ 'paper_id' => $paperId, 'stats' => $finalStats, 'assemble_elapsed_ms' => (int) round((microtime(true) - $assembleStartedAt) * 1000), ]); $taskManager->updateTaskProgress($this->taskId, 40, '组卷完成,开始生成PDF...'); $phaseStartedAt = microtime(true); dispatch(new GenerateExamPdfJob($this->taskId, $paperId)); Log::info('assemble.job.timing', [ 'task_id' => $this->taskId, 'phase' => 'dispatch_pdf_job', 'elapsed_ms' => (int) round((microtime(true) - $phaseStartedAt) * 1000), 'paper_id' => $paperId, ]); Log::info('assemble.success', [ 'task_id' => $this->taskId, 'paper_id' => $paperId, 'assemble_type' => $finalAssembleType, 'question_count' => count($questions), 'total_score' => $totalScore, 'elapsed_ms' => (int) round((microtime(true) - $assembleStartedAt) * 1000), ]); } catch (\Exception $e) { Log::error('AssembleExamTaskJob: 异常', [ 'task_id' => $this->taskId, 'error' => $e->getMessage(), ]); $taskManager->markTaskFailed($this->taskId, $e->getMessage()); } } public function failed(Throwable $exception): void { app(TaskManager::class)->markTaskFailed($this->taskId, $exception->getMessage()); } private function normalizeQuestionTypeRatio(array $input): array { $defaults = ['选择题' => 40, '填空题' => 20, '解答题' => 40]; $normalized = []; foreach ($input as $key => $value) { if (! is_numeric($value)) { continue; } $type = $this->normalizeQuestionTypeKey((string) $key); if ($type) { $normalized[$type] = (float) $value; } } $merged = array_merge($defaults, $normalized); $sum = array_sum($merged); if ($sum > 0) { foreach ($merged as $k => $v) { $merged[$k] = round(($v / $sum) * 100, 2); } } return $merged; } private function normalizeQuestionTypeKey(string $key): ?string { $key = trim($key); if (in_array($key, ['choice', '选择题', 'single_choice', 'multiple_choice', 'CHOICE', 'SINGLE_CHOICE', 'MULTIPLE_CHOICE'], true)) { return '选择题'; } if (in_array($key, ['fill', '填空题', 'blank', 'FILL_IN_THE_BLANK', 'FILL'], true)) { return '填空题'; } if (in_array($key, ['answer', '解答题', '计算题', 'CALCULATION', 'WORD_PROBLEM', 'PROOF'], true)) { return '解答题'; } return null; } private function resolveMistakeQuestionIds(string $studentId, array $mistakeIds, array $mistakeQuestionIds): array { $questionIds = []; if (! empty($mistakeQuestionIds)) { $questionIds = array_merge($questionIds, $mistakeQuestionIds); } if (! empty($mistakeIds)) { $fromDb = MistakeRecord::query()->where('student_id', $studentId)->whereIn('id', $mistakeIds)->pluck('question_id')->filter()->values()->all(); $questionIds = array_merge($questionIds, $fromDb); } return array_values(array_unique(array_filter($questionIds))); } /** * 追练(assemble_type=5)+ 指定错题:mistake_ids 须逐条命中该学生的 mistake_records; * mistake_question_ids 须在该学生错题本中至少有一条记录。顺序:先按 mistake_ids 请求顺序,再追加题号列表(去重)。 * assemble_type=15(错题再练)将 paper_ids 解析为题库题目 id 后,仅使用本方法的 mistake_question_ids 分支做校验。 * * @return array{ok: bool, message?: string, question_ids?: array} */ private function resolveMistakeQuestionIdsStrictForStudent(string $studentId, array $mistakeIds, array $mistakeQuestionIds): array { $mistakeIds = array_values(array_filter(array_map('strval', $mistakeIds), fn ($v) => $v !== '')); $mistakeQuestionIds = array_values(array_filter(array_map('strval', $mistakeQuestionIds), fn ($v) => $v !== '')); $orderedQuestionIds = []; $seen = []; if ($mistakeIds !== []) { $rowIdSet = array_values(array_unique($mistakeIds)); $records = MistakeRecord::query() ->where('student_id', $studentId) ->whereIn('id', $rowIdSet) ->get() ->keyBy(fn ($r) => (string) $r->id); foreach ($mistakeIds as $mid) { $rec = $records[$mid] ?? null; $qid = $rec && $rec->question_id !== null && $rec->question_id !== '' ? (string) $rec->question_id : ''; if ($qid === '') { return [ 'ok' => false, 'message' => '部分错题记录不存在或不属于该学生: '.$mid, ]; } if (! isset($seen[$qid])) { $seen[$qid] = true; $orderedQuestionIds[] = $qid; } } } foreach ($mistakeQuestionIds as $qid) { $exists = MistakeRecord::query() ->where('student_id', $studentId) ->where('question_id', $qid) ->exists(); if (! $exists) { return [ 'ok' => false, 'message' => '学生错题本中不存在题目: '.$qid, ]; } if (! isset($seen[$qid])) { $seen[$qid] = true; $orderedQuestionIds[] = $qid; } } return ['ok' => true, 'question_ids' => $orderedQuestionIds]; } /** * assemble_type=15/16 时 paper_ids 承载题库题目 id:纯数字字符串转为 int,去重并保持首次出现顺序。 * * @return array */ private function normalizeBankQuestionIdsList(array $raw): array { $out = []; $seen = []; foreach ($raw as $v) { if ($v === null) { continue; } if (is_string($v)) { $v = trim($v); if ($v === '') { continue; } } if (is_int($v)) { $normalized = $v; } elseif (is_float($v) && floor($v) == $v) { $normalized = (int) $v; } else { $s = trim((string) $v); if ($s === '') { continue; } $normalized = preg_match('/^-?\d+$/', $s) ? (int) $s : $s; } $dedupeKey = is_int($normalized) ? 'i:'.$normalized : 's:'.(string) $normalized; if (isset($seen[$dedupeKey])) { continue; } $seen[$dedupeKey] = true; $out[] = $normalized; } return $out; } private function hydrateQuestions(array $questions, array $kpCodes): array { $mapper = app(QuestionPayloadMapper::class); $normalized = []; foreach ($questions as $question) { $normalized[] = $mapper->fromArray($question, $kpCodes); } return array_values(array_filter($normalized, fn ($q) => ! empty($q['id']))); } private function sortQuestionsByRequestedIds(array $questions, array $requestedIds): array { if (empty($requestedIds)) { return $questions; } $order = array_flip($requestedIds); usort($questions, function ($a, $b) use ($order) { $aPos = $order[(string) ($a['id'] ?? '')] ?? PHP_INT_MAX; $bPos = $order[(string) ($b['id'] ?? '')] ?? PHP_INT_MAX; return $aPos <=> $bPos; }); return $questions; } private function sortQuestionsWithinTypeByDifficulty(array $questions): array { $grouped = ['choice' => [], 'fill' => [], 'answer' => []]; foreach ($questions as $question) { $type = $this->normalizeQuestionType((string) ($question['question_type'] ?? 'answer')); $grouped[$type][] = $question; } $sortFn = function (array $a, array $b): int { $ad = (float) ($a['difficulty'] ?? 0.5); $bd = (float) ($b['difficulty'] ?? 0.5); if ($ad !== $bd) { return $ad <=> $bd; } return ((int) ($a['id'] ?? $a['question_id'] ?? 0)) <=> ((int) ($b['id'] ?? $b['question_id'] ?? 0)); }; usort($grouped['choice'], $sortFn); usort($grouped['fill'], $sortFn); usort($grouped['answer'], $sortFn); $sorted = array_merge($grouped['choice'], $grouped['fill'], $grouped['answer']); foreach ($sorted as $idx => &$question) { $question['question_number'] = $idx + 1; } unset($question); return $sorted; } private function deriveDifficultyCategoryFromSelectedDistribution(array $questions): int { if ($questions === []) { return 1; } $service = app(DifficultyDistributionService::class); $total = count($questions); $bestCategory = 1; $bestScore = null; foreach ([0, 1, 2, 3, 4] as $category) { $actualBuckets = $service->groupQuestionsByDifficultyRange($questions, $category); $expectedBuckets = $this->expectedDifficultyBucketCounts($service, $category, $total); $score = 0; foreach (['primary_low', 'primary_medium', 'primary_high', 'secondary', 'other'] as $bucketKey) { $score += abs(count($actualBuckets[$bucketKey] ?? []) - ($expectedBuckets[$bucketKey] ?? 0)); } if ($bestScore === null || $score < $bestScore) { $bestScore = $score; $bestCategory = $category; } } return $bestCategory; } /** * @return array{primary_low: int, primary_medium: int, primary_high: int, secondary: int, other: int} */ private function expectedDifficultyBucketCounts(DifficultyDistributionService $service, int $category, int $totalQuestions): array { $expected = [ 'primary_low' => 0, 'primary_medium' => 0, 'primary_high' => 0, 'secondary' => 0, 'other' => 0, ]; foreach ($service->calculateDistribution($category, $totalQuestions) as $level => $config) { $bucketKey = $service->mapDifficultyLevelToRangeKey((string) $level, $category); $expected[$bucketKey] = ($expected[$bucketKey] ?? 0) + (int) ($config['count'] ?? 0); } return $expected; } private function averageQuestionDifficulty(array $questions): float { if ($questions === []) { return 0.0; } $sum = 0.0; foreach ($questions as $question) { $difficulty = $question['difficulty'] ?? 0.0; $value = is_numeric($difficulty) ? (float) $difficulty : 0.0; if ($value > 1) { $value = $value / 5; } $sum += max(0.0, min(1.0, $value)); } return round($sum / count($questions), 4); } private function normalizeQuestionType(string $type): string { $type = strtolower(trim($type)); if (in_array($type, ['choice', 'single_choice', 'multiple_choice', '选择题', '单选', '多选'], true)) { return 'choice'; } if (in_array($type, ['fill', 'fill_in_the_blank', 'blank', '填空题', '填空'], true)) { return 'fill'; } return 'answer'; } private function adjustQuestionScores(array $questions, float $targetTotalScore = 100.0): array { if (empty($questions)) { return $questions; } // 第一步:按题型排序 $sortedQuestions = []; $choiceQuestions = []; $fillQuestions = []; $answerQuestions = []; foreach ($questions as $question) { $type = $this->normalizeQuestionType($question['question_type'] ?? 'answer'); if ($type === 'choice') { $choiceQuestions[] = $question; } elseif ($type === 'fill') { $fillQuestions[] = $question; } else { $answerQuestions[] = $question; } } $sortedQuestions = array_merge($choiceQuestions, $fillQuestions, $answerQuestions); Log::debug('adjustQuestionScores 开始', [ 'choice_count' => count($choiceQuestions), 'fill_count' => count($fillQuestions), 'answer_count' => count($answerQuestions), ]); foreach ($sortedQuestions as $idx => &$question) { $question['question_number'] = $idx + 1; } unset($question); $typeCounts = [ 'choice' => count($choiceQuestions), 'fill' => count($fillQuestions), 'answer' => count($answerQuestions), ]; $typeIndexes = ['choice' => [], 'fill' => [], 'answer' => []]; foreach ($sortedQuestions as $index => $question) { $type = $this->normalizeQuestionType($question['question_type'] ?? 'answer'); $typeIndexes[$type][] = $index; } $questionScores = []; $totalQuestions = $typeCounts['choice'] + $typeCounts['fill'] + $typeCounts['answer']; $globalBaseScore = floor($targetTotalScore / $totalQuestions); $globalBaseScore = max(1, $globalBaseScore); $typeOrder = []; foreach ($sortedQuestions as $question) { $type = $this->normalizeQuestionType($question['question_type'] ?? 'answer'); if (! in_array($type, $typeOrder)) { $typeOrder[] = $type; } } $remainingBudget = $targetTotalScore; foreach ($typeOrder as $typeIndex => $type) { $count = $typeCounts[$type]; if ($count === 0) { continue; } if ($typeIndex === 0) { $thisBase = $globalBaseScore; foreach ($typeIndexes[$type] as $idx) { $questionScores[$idx] = $thisBase; } foreach ($typeIndexes[$type] as $idx) { $questionScores[$idx] = max(1, $questionScores[$idx] - 1); } $allocated = 0; foreach ($typeIndexes[$type] as $idx) { $allocated += $questionScores[$idx]; } $remainingBudget -= $allocated; } elseif ($typeIndex === count($typeOrder) - 1) { $thisBase = floor($remainingBudget / $count); $thisBase = max(1, $thisBase); foreach ($typeIndexes[$type] as $idx) { $questionScores[$idx] = $thisBase; } $total = $thisBase * $count; $remainder = $remainingBudget - $total; if ($remainder > 0) { $answerIndexes = array_values($typeIndexes[$type]); $startIdx = max(0, count($answerIndexes) - $remainder); for ($i = $startIdx; $i < count($answerIndexes); $i++) { $questionScores[$answerIndexes[$i]] += 1; } } } else { $thisBase = $globalBaseScore; foreach ($typeIndexes[$type] as $idx) { $questionScores[$idx] = $thisBase; } $allocated = 0; foreach ($typeIndexes[$type] as $idx) { $allocated += $questionScores[$idx]; } $remainingBudget -= $allocated; } } if (count($typeOrder) > 1) { $lastType = end($typeOrder); $otherTypes = array_slice($typeOrder, 0, -1); $maxOtherScore = 0; foreach ($otherTypes as $type) { foreach ($typeIndexes[$type] as $idx) { $maxOtherScore = max($maxOtherScore, $questionScores[$idx]); } } $minLastScore = PHP_INT_MAX; foreach ($typeIndexes[$lastType] as $idx) { $minLastScore = min($minLastScore, $questionScores[$idx]); } if ($minLastScore <= $maxOtherScore) { $diff = $maxOtherScore - $minLastScore + 1; $reductionPerQuestion = min($diff, 2); foreach ($otherTypes as $type) { foreach ($typeIndexes[$type] as $idx) { $questionScores[$idx] = max(1, $questionScores[$idx] - $reductionPerQuestion); } } $reallocated = $targetTotalScore; foreach ($typeIndexes[$lastType] as $idx) { $reallocated -= $questionScores[$idx]; } foreach ($otherTypes as $type) { foreach ($typeIndexes[$type] as $idx) { $reallocated -= $questionScores[$idx]; } } if ($reallocated > 0) { $newBase = floor($reallocated / $typeCounts[$lastType]); foreach ($typeIndexes[$lastType] as $idx) { $questionScores[$idx] = $newBase; } $total = $newBase * $typeCounts[$lastType]; $remainder = $reallocated - $total; if ($remainder > 0) { $lastIndexes = array_values($typeIndexes[$lastType]); $startIdx = max(0, count($lastIndexes) - $remainder); for ($i = $startIdx; $i < count($lastIndexes); $i++) { $questionScores[$lastIndexes[$i]] += 1; } } } } } $adjustedQuestions = []; foreach ($sortedQuestions as $index => $question) { $adjustedQuestions[$index] = $question; $adjustedQuestions[$index]['score'] = $questionScores[$index] ?? 5; } $total = array_sum(array_column($adjustedQuestions, 'score')); $diff = (int) $targetTotalScore - (int) $total; if ($diff !== 0 && ! empty($adjustedQuestions)) { $count = count($adjustedQuestions); $i = $count - 1; while ($diff !== 0) { $score = $adjustedQuestions[$i]['score']; if ($diff > 0) { $adjustedQuestions[$i]['score'] = $score + 1; $diff--; } else { if ($score > 1) { $adjustedQuestions[$i]['score'] = $score - 1; $diff++; } } $i--; if ($i < 0) { $i = $count - 1; if ($diff < 0) { $minScore = min(array_column($adjustedQuestions, 'score')); if ($minScore <= 1) { break; } } } } } return $adjustedQuestions; } }