|
|
@@ -1081,6 +1081,180 @@ class ExamPdfController extends Controller
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 按题号顺序从试卷题目反推知识点编码,去重(集合顺序用于同级兜底)。
|
|
|
+ * 展示顺序由 {@see sortKnowledgePointCodesByHierarchy} 按知识点树 DFS 决定。
|
|
|
+ *
|
|
|
+ * @return array<int,string>
|
|
|
+ */
|
|
|
+ private function extractKnowledgePointCodesFromPaperQuestions(string $paperId): array
|
|
|
+ {
|
|
|
+ $paperQuestions = \App\Models\PaperQuestion::query()
|
|
|
+ ->where('paper_id', $paperId)
|
|
|
+ ->orderBy('question_number')
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ if ($paperQuestions->isEmpty()) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $questionBankIds = $paperQuestions
|
|
|
+ ->pluck('question_bank_id')
|
|
|
+ ->filter()
|
|
|
+ ->unique()
|
|
|
+ ->values();
|
|
|
+
|
|
|
+ $questionKpMap = [];
|
|
|
+ if ($questionBankIds->isNotEmpty()) {
|
|
|
+ $questionKpMap = \App\Models\Question::query()
|
|
|
+ ->whereIn('id', $questionBankIds)
|
|
|
+ ->pluck('kp_code', 'id')
|
|
|
+ ->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ $ordered = [];
|
|
|
+ $seen = [];
|
|
|
+
|
|
|
+ foreach ($paperQuestions as $pq) {
|
|
|
+ $kpCode = trim((string) ($pq->knowledge_point ?? ''));
|
|
|
+ if ($kpCode === '' && ! empty($pq->question_bank_id)) {
|
|
|
+ $kpCode = trim((string) ($questionKpMap[$pq->question_bank_id] ?? ''));
|
|
|
+ }
|
|
|
+ if ($kpCode === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (isset($seen[$kpCode])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $seen[$kpCode] = true;
|
|
|
+ $ordered[] = $kpCode;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $ordered;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * papers.params 中的 kp_codes / kp_code_list(仅作无 paper_questions 时的兜底,例如未入库题目的讲解卷)。
|
|
|
+ *
|
|
|
+ * @param array<string,mixed>|null $params
|
|
|
+ * @return array<int,string>
|
|
|
+ */
|
|
|
+ private function kpCodesFromAssembleRequestPayload(?array $params): array
|
|
|
+ {
|
|
|
+ if ($params === null || $params === []) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $seen = [];
|
|
|
+ foreach (['kp_codes', 'kp_code_list'] as $key) {
|
|
|
+ if (! isset($params[$key])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $raw = $params[$key];
|
|
|
+ if (is_string($raw)) {
|
|
|
+ $parts = array_filter(array_map('trim', explode(',', $raw)));
|
|
|
+ foreach ($parts as $c) {
|
|
|
+ if ($c !== '') {
|
|
|
+ $seen[$c] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } elseif (is_array($raw)) {
|
|
|
+ foreach ($raw as $c) {
|
|
|
+ $c = trim((string) $c);
|
|
|
+ if ($c !== '') {
|
|
|
+ $seen[$c] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return array_keys($seen);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按知识点树层级排序:根在前,同层兄弟按 kp_code 字典序,深度优先。
|
|
|
+ * 库中不存在的编码保留在末尾,顺序与原列表一致。
|
|
|
+ *
|
|
|
+ * @param array<int,string> $kpCodes
|
|
|
+ * @return array<int,string>
|
|
|
+ */
|
|
|
+ private function sortKnowledgePointCodesByHierarchy(array $kpCodes): array
|
|
|
+ {
|
|
|
+ $kpCodes = array_values(array_unique(array_filter(array_map(static fn ($c) => trim((string) $c), $kpCodes), static fn ($c) => $c !== '')));
|
|
|
+ if (count($kpCodes) <= 1) {
|
|
|
+ return $kpCodes;
|
|
|
+ }
|
|
|
+
|
|
|
+ $byCode = [];
|
|
|
+ $pending = $kpCodes;
|
|
|
+ $guard = 0;
|
|
|
+ while ($pending !== [] && $guard < 100) {
|
|
|
+ $guard++;
|
|
|
+ $rows = \App\Models\KnowledgePoint::query()
|
|
|
+ ->whereIn('kp_code', $pending)
|
|
|
+ ->get()
|
|
|
+ ->keyBy('kp_code');
|
|
|
+ $next = [];
|
|
|
+ foreach ($rows as $code => $row) {
|
|
|
+ $byCode[$code] = $row;
|
|
|
+ $p = trim((string) ($row->parent_kp_code ?? ''));
|
|
|
+ if ($p !== '' && ! isset($byCode[$p])) {
|
|
|
+ $next[$p] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $pending = array_keys($next);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($byCode === []) {
|
|
|
+ return $kpCodes;
|
|
|
+ }
|
|
|
+
|
|
|
+ $childrenByParent = [];
|
|
|
+ foreach ($byCode as $code => $row) {
|
|
|
+ $p = trim((string) ($row->parent_kp_code ?? ''));
|
|
|
+ if ($p !== '' && isset($byCode[$p])) {
|
|
|
+ $childrenByParent[$p][] = $code;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ foreach ($childrenByParent as &$kids) {
|
|
|
+ sort($kids, SORT_STRING);
|
|
|
+ }
|
|
|
+ unset($kids);
|
|
|
+
|
|
|
+ $roots = [];
|
|
|
+ foreach ($byCode as $code => $row) {
|
|
|
+ $p = trim((string) ($row->parent_kp_code ?? ''));
|
|
|
+ if ($p === '' || ! isset($byCode[$p])) {
|
|
|
+ $roots[] = $code;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sort($roots, SORT_STRING);
|
|
|
+
|
|
|
+ $want = array_flip($kpCodes);
|
|
|
+ $out = [];
|
|
|
+
|
|
|
+ $visit = function (string $node) use (&$visit, &$out, &$want, &$childrenByParent): void {
|
|
|
+ if (isset($want[$node])) {
|
|
|
+ $out[] = $node;
|
|
|
+ }
|
|
|
+ foreach ($childrenByParent[$node] ?? [] as $child) {
|
|
|
+ $visit($child);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ foreach ($roots as $r) {
|
|
|
+ $visit($r);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($kpCodes as $c) {
|
|
|
+ if (! isset($byCode[$c])) {
|
|
|
+ $out[] = $c;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 知识点讲解视图
|
|
|
*/
|
|
|
@@ -1099,40 +1273,24 @@ class ExamPdfController extends Controller
|
|
|
// 生成时间(格式:2026年01月30日 15:04:05)
|
|
|
$generateDateTime = now()->format('Y年m月d日 H:i:s');
|
|
|
|
|
|
- // 优先使用 paper 中保存的 explanation_kp_codes(组卷时指定的知识点,最多2个)
|
|
|
- $kpCodes = $paper->explanation_kp_codes ?? [];
|
|
|
-
|
|
|
- // 如果没有保存 explanation_kp_codes,回退到从题目中提取(兼容旧数据)
|
|
|
- if (empty($kpCodes)) {
|
|
|
- $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id)->get();
|
|
|
- $seen = [];
|
|
|
-
|
|
|
- $questionBankIds = $paperQuestions
|
|
|
- ->pluck('question_bank_id')
|
|
|
- ->filter()
|
|
|
- ->unique()
|
|
|
- ->values();
|
|
|
- $questionKpMap = [];
|
|
|
- if ($questionBankIds->isNotEmpty()) {
|
|
|
- $questionKpMap = \App\Models\Question::whereIn('id', $questionBankIds)
|
|
|
- ->pluck('kp_code', 'id')
|
|
|
- ->toArray();
|
|
|
- }
|
|
|
+ // 1) 主路径:由题目反推知识点(去重后有几个展示几块)
|
|
|
+ $kpCodes = $this->extractKnowledgePointCodesFromPaperQuestions((string) $paper_id);
|
|
|
|
|
|
- foreach ($paperQuestions as $pq) {
|
|
|
- $kpCode = trim((string) ($pq->knowledge_point ?? ''));
|
|
|
- if ($kpCode === '' && ! empty($pq->question_bank_id)) {
|
|
|
- $kpCode = trim((string) ($questionKpMap[$pq->question_bank_id] ?? ''));
|
|
|
- }
|
|
|
- if ($kpCode === '') {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (isset($seen[$kpCode])) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- $seen[$kpCode] = true;
|
|
|
- $kpCodes[] = $kpCode;
|
|
|
- }
|
|
|
+ // 2) 无题目行时:策略层 explanation_kp_codes
|
|
|
+ if ($kpCodes === []) {
|
|
|
+ $kpCodes = $paper->explanation_kp_codes ?? [];
|
|
|
+ }
|
|
|
+ if (! is_array($kpCodes)) {
|
|
|
+ $kpCodes = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3) 仍为空:params 快照(无 paper_questions 的边界场景)
|
|
|
+ if ($kpCodes === []) {
|
|
|
+ $kpCodes = $this->kpCodesFromAssembleRequestPayload(is_array($paper->params) ? $paper->params : null);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count($kpCodes) > 1) {
|
|
|
+ $kpCodes = $this->sortKnowledgePointCodesByHierarchy($kpCodes);
|
|
|
}
|
|
|
|
|
|
// 使用 ExamPdfExportService 构建知识点数据
|