|
|
@@ -0,0 +1,533 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Jobs;
|
|
|
+
|
|
|
+use App\Models\MistakeRecord;
|
|
|
+use App\Services\LearningAnalyticsService;
|
|
|
+use App\Services\QuestionBankService;
|
|
|
+use App\Services\TaskManager;
|
|
|
+use Illuminate\Bus\Queueable;
|
|
|
+use Illuminate\Contracts\Queue\ShouldQueue;
|
|
|
+use Illuminate\Foundation\Bus\Dispatchable;
|
|
|
+use Illuminate\Queue\InteractsWithQueue;
|
|
|
+use Illuminate\Queue\SerializesModels;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+use Throwable;
|
|
|
+
|
|
|
+class AssembleExamTaskJob implements ShouldQueue
|
|
|
+{
|
|
|
+ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
+
|
|
|
+ public string $taskId;
|
|
|
+
|
|
|
+ public int $tries = 2;
|
|
|
+
|
|
|
+ public int $timeout = 180;
|
|
|
+
|
|
|
+ public function __construct(string $taskId)
|
|
|
+ {
|
|
|
+ $this->taskId = $taskId;
|
|
|
+ // 复用现有 pdf-worker,避免 default 队列无人消费
|
|
|
+ $this->onQueue('pdf');
|
|
|
+ $this->afterCommit();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function handle(
|
|
|
+ LearningAnalyticsService $learningAnalyticsService,
|
|
|
+ QuestionBankService $questionBankService,
|
|
|
+ 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);
|
|
|
+
|
|
|
+ try {
|
|
|
+ $taskManager->updateTaskProgress($this->taskId, 5, '开始异步组卷...');
|
|
|
+
|
|
|
+ $assembleType = (int) ($data['assemble_type'] ?? 4);
|
|
|
+ $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;
|
|
|
+
|
|
|
+ if (! empty($mistakeIds) || ! empty($mistakeQuestionIds)) {
|
|
|
+ $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 = $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' => $assembleType === 3 ? null : ($data['kp_codes'] ?? null),
|
|
|
+ 'skills' => $data['skills'] ?? [],
|
|
|
+ 'question_type_ratio' => $questionTypeRatio,
|
|
|
+ 'difficulty_category' => $difficultyCategory,
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ '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' => $assembleType === 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'] ?? []);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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'])) ? $result['assemble_type'] : $assembleType;
|
|
|
+ $paperId = $questionBankService->saveExamToDatabase([
|
|
|
+ 'paper_id' => $data['paper_id'] ?? null,
|
|
|
+ 'paper_name' => $paperName,
|
|
|
+ 'student_id' => $data['student_id'],
|
|
|
+ 'teacher_id' => $data['teacher_id'] ?? null,
|
|
|
+ '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;
|
|
|
+ }
|
|
|
+
|
|
|
+ $finalStats = $result['stats'] ?? [
|
|
|
+ 'total_selected' => count($questions),
|
|
|
+ 'mistake_based' => ! empty($mistakeIds) || ! empty($mistakeQuestionIds),
|
|
|
+ ];
|
|
|
+ if (! isset($finalStats['difficulty_category'])) {
|
|
|
+ $finalStats['difficulty_category'] = $difficultyCategory;
|
|
|
+ }
|
|
|
+
|
|
|
+ $taskManager->updateTaskStatus($this->taskId, [
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ 'stats' => $finalStats,
|
|
|
+ 'assemble_elapsed_ms' => (int) round((microtime(true) - $assembleStartedAt) * 1000),
|
|
|
+ ]);
|
|
|
+ $taskManager->updateTaskProgress($this->taskId, 40, '组卷完成,开始生成PDF...');
|
|
|
+
|
|
|
+ dispatch(new GenerateExamPdfJob($this->taskId, $paperId));
|
|
|
+ Log::info('AssembleExamTaskJob: 组卷任务完成并已触发PDF任务', [
|
|
|
+ 'task_id' => $this->taskId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ ]);
|
|
|
+ } 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)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private function hydrateQuestions(array $questions, array $kpCodes): array
|
|
|
+ {
|
|
|
+ $normalized = [];
|
|
|
+ foreach ($questions as $question) {
|
|
|
+ $type = $this->normalizeQuestionTypeKey($question['question_type'] ?? $question['type'] ?? '') ?? $this->guessType($question);
|
|
|
+ $score = $question['score'] ?? $this->defaultScore($type);
|
|
|
+ $normalized[] = [
|
|
|
+ 'id' => $question['id'] ?? $question['question_id'] ?? null,
|
|
|
+ 'question_id' => $question['question_id'] ?? null,
|
|
|
+ 'question_type' => $type === '选择题' ? 'choice' : ($type === '填空题' ? 'fill' : 'answer'),
|
|
|
+ 'stem' => $question['stem'] ?? $question['content'] ?? ($question['question_text'] ?? ''),
|
|
|
+ 'content' => $question['content'] ?? $question['stem'] ?? '',
|
|
|
+ 'options' => $question['options'] ?? ($question['choices'] ?? []),
|
|
|
+ 'answer' => $question['answer'] ?? $question['correct_answer'] ?? '',
|
|
|
+ 'solution' => $question['solution'] ?? '',
|
|
|
+ 'difficulty' => isset($question['difficulty']) ? (float) $question['difficulty'] : 0.5,
|
|
|
+ 'score' => $score,
|
|
|
+ 'estimated_time' => $question['estimated_time'] ?? 300,
|
|
|
+ 'kp' => $question['kp_code'] ?? $question['kp'] ?? $question['knowledge_point'] ?? ($kpCodes[0] ?? ''),
|
|
|
+ 'kp_code' => $question['kp_code'] ?? $question['kp'] ?? $question['knowledge_point'] ?? ($kpCodes[0] ?? ''),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ 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 guessType(array $question): string
|
|
|
+ {
|
|
|
+ if (! empty($question['options']) && is_array($question['options'])) {
|
|
|
+ return '选择题';
|
|
|
+ }
|
|
|
+ $content = $question['stem'] ?? $question['content'] ?? '';
|
|
|
+ if (is_string($content) && (strpos($content, '____') !== false || strpos($content, '()') !== false)) {
|
|
|
+ return '填空题';
|
|
|
+ }
|
|
|
+ return '解答题';
|
|
|
+ }
|
|
|
+
|
|
|
+ private function defaultScore(string $type): int
|
|
|
+ {
|
|
|
+ return match ($type) {
|
|
|
+ '选择题' => 5,
|
|
|
+ '填空题' => 5,
|
|
|
+ '解答题' => 10,
|
|
|
+ default => 5,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 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;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|