|
|
@@ -72,7 +72,8 @@ class ExamPdfExportService
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- $path = "analysis_reports/{$paperId}_{$studentId}.pdf";
|
|
|
+ $version = time();
|
|
|
+ $path = "analysis_reports/{$paperId}_{$studentId}_{$version}.pdf";
|
|
|
Storage::disk('public')->put($path, $pdfBinary);
|
|
|
|
|
|
return URL::to(Storage::url($path));
|
|
|
@@ -239,6 +240,8 @@ class ExamPdfExportService
|
|
|
]);
|
|
|
$process->setTimeout(60);
|
|
|
|
|
|
+ $killSignal = \defined('SIGKILL') ? \SIGKILL : 9;
|
|
|
+
|
|
|
try {
|
|
|
$startedAt = microtime(true);
|
|
|
Log::info('ExamPdfExportService: Chrome 渲染启动', [
|
|
|
@@ -263,7 +266,7 @@ class ExamPdfExportService
|
|
|
'duration_sec' => round(microtime(true) - $startedAt, 3),
|
|
|
'tmp_pdf_size' => filesize($tmpPdf),
|
|
|
]);
|
|
|
- $process->stop(5, SIGKILL);
|
|
|
+ $process->stop(5, $killSignal);
|
|
|
break;
|
|
|
}
|
|
|
usleep(200_000); // 200ms
|
|
|
@@ -274,7 +277,7 @@ class ExamPdfExportService
|
|
|
Log::warning('ExamPdfExportService: Chrome 轮询超时,强制结束', [
|
|
|
'duration_sec' => round(microtime(true) - $startedAt, 3),
|
|
|
]);
|
|
|
- $process->stop(5, SIGKILL);
|
|
|
+ $process->stop(5, $killSignal);
|
|
|
}
|
|
|
|
|
|
$process->wait();
|
|
|
@@ -298,7 +301,7 @@ class ExamPdfExportService
|
|
|
'trace' => $e->getTraceAsString(),
|
|
|
]);
|
|
|
if ($process->isRunning()) {
|
|
|
- $process->stop(5, SIGKILL);
|
|
|
+ $process->stop(5, $killSignal);
|
|
|
}
|
|
|
$pdfExists = file_exists($tmpPdf);
|
|
|
$pdfSize = $pdfExists ? filesize($tmpPdf) : null;
|
|
|
@@ -326,7 +329,7 @@ class ExamPdfExportService
|
|
|
'trace' => $e->getTraceAsString(),
|
|
|
]);
|
|
|
if ($process->isRunning()) {
|
|
|
- $process->stop(5, SIGKILL);
|
|
|
+ $process->stop(5, $killSignal);
|
|
|
}
|
|
|
$pdfExists = file_exists($tmpPdf);
|
|
|
$pdfSize = $pdfExists ? filesize($tmpPdf) : null;
|
|
|
@@ -387,7 +390,7 @@ class ExamPdfExportService
|
|
|
private function buildAnalysisPayload(string $paperId, string $studentId): ?array
|
|
|
{
|
|
|
$paper = Paper::with(['questions' => function ($query) {
|
|
|
- $query->orderBy('question_number');
|
|
|
+ $query->orderBy('question_number')->orderBy('id');
|
|
|
}])->find($paperId);
|
|
|
if (!$paper) {
|
|
|
Log::error('ExamPdfExportService: 未找到试卷,无法生成学情报告', [
|
|
|
@@ -445,17 +448,31 @@ class ExamPdfExportService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- $questions = $paper->questions
|
|
|
- ->map(function (PaperQuestion $question) use ($kpNameMap, $questionDetails) {
|
|
|
+ // 分组保持卷面顺序:选择题 -> 填空题 -> 解答题
|
|
|
+ $grouped = [
|
|
|
+ 'choice' => [],
|
|
|
+ 'fill' => [],
|
|
|
+ 'answer' => [],
|
|
|
+ ];
|
|
|
+
|
|
|
+ $sortedQuestions = $paper->questions
|
|
|
+ ->sortBy(function (PaperQuestion $q, int $idx) {
|
|
|
+ $number = $q->question_number ?? $idx + 1;
|
|
|
+ return is_numeric($number) ? (float) $number : ($q->id ?? $idx);
|
|
|
+ });
|
|
|
+
|
|
|
+ foreach ($sortedQuestions as $idx => $question) {
|
|
|
$kpCode = $question->knowledge_point ?? '';
|
|
|
$kpName = $kpNameMap[$kpCode] ?? $kpCode ?: '未标注';
|
|
|
$detail = $questionDetails[(string) ($question->question_id ?? '')] ?? [];
|
|
|
$solution = $detail['solution'] ?? $detail['解析'] ?? $detail['analysis'] ?? null;
|
|
|
- $typeRaw = $detail['question_type'] ?? $detail['type'] ?? $question->question_type ?? '';
|
|
|
+ // 题型优先使用试卷记录,其次题库详情
|
|
|
+ $typeRaw = $question->question_type ?? ($detail['question_type'] ?? $detail['type'] ?? '');
|
|
|
$normalizedType = $this->normalizeQuestionType($typeRaw);
|
|
|
+ $number = $question->question_number ?? ($idx + 1);
|
|
|
|
|
|
- return [
|
|
|
- 'question_number' => $question->question_number,
|
|
|
+ $payload = [
|
|
|
+ 'question_number' => $number,
|
|
|
'question_text' => is_array($question->question_text) ? json_encode($question->question_text, JSON_UNESCAPED_UNICODE) : ($question->question_text ?? ''),
|
|
|
'question_type' => $normalizedType,
|
|
|
'knowledge_point' => $kpCode,
|
|
|
@@ -463,10 +480,19 @@ class ExamPdfExportService
|
|
|
'score' => $question->score,
|
|
|
'solution' => $solution,
|
|
|
];
|
|
|
- })
|
|
|
- ->sortBy('question_number')
|
|
|
- ->values()
|
|
|
- ->toArray();
|
|
|
+
|
|
|
+ $grouped[$normalizedType][] = $payload;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ordered = array_merge($grouped['choice'], $grouped['fill'], $grouped['answer']);
|
|
|
+
|
|
|
+ // 按卷面顺序重新编号以匹配判卷/显示
|
|
|
+ foreach ($ordered as $i => &$q) {
|
|
|
+ $q['display_number'] = $i + 1;
|
|
|
+ }
|
|
|
+ unset($q);
|
|
|
+
|
|
|
+ $questions = $ordered;
|
|
|
|
|
|
$questionInsights = $analysisData['question_results'] ?? [];
|
|
|
$masterySummary = $this->buildMasterySummary($masteryData, $kpNameMap);
|