|
|
@@ -3,6 +3,7 @@
|
|
|
namespace App\Services;
|
|
|
|
|
|
use App\Models\Question;
|
|
|
+use App\Support\AnswerSolutionStepMarkerInjector;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
use Illuminate\Support\Str;
|
|
|
@@ -39,7 +40,7 @@ class QuestionTemReviewService
|
|
|
* 左侧:按 questions 表中该知识点正式题数量升序(题少的在前),仅包含 questions_tem 中出现过的 kp_code
|
|
|
*
|
|
|
* @param ?int $limit 为 null 时不截断(质检页需完整列表 + 搜索,否则题量大的 KP 如 B01 会落在 500 条之后而无法检索)
|
|
|
- * @return list<array{kp_code: string, kp_name: string, questions_count: int, tem_count: int}>
|
|
|
+ * @return list<array{kp_code: string, kp_name: string, questions_count: int, tem_count: int, tem_importable_count: int}>
|
|
|
*/
|
|
|
public function listKnowledgePointsByQuestionsAsc(?int $limit = null): array
|
|
|
{
|
|
|
@@ -83,6 +84,8 @@ class QuestionTemReviewService
|
|
|
->pluck('c', 'kp_code')
|
|
|
->toArray();
|
|
|
|
|
|
+ $importableMap = $this->importableTemCountsForKpCodes($temKps);
|
|
|
+
|
|
|
$rows = [];
|
|
|
foreach ($temKps as $kp) {
|
|
|
$name = isset($kpNames[$kp]) ? trim((string) $kpNames[$kp]) : '';
|
|
|
@@ -91,6 +94,7 @@ class QuestionTemReviewService
|
|
|
'kp_name' => $name,
|
|
|
'questions_count' => (int) ($counts[$kp] ?? 0),
|
|
|
'tem_count' => (int) ($temCounts[$kp] ?? 0),
|
|
|
+ 'tem_importable_count' => (int) ($importableMap[$kp] ?? 0),
|
|
|
];
|
|
|
}
|
|
|
|
|
|
@@ -110,13 +114,58 @@ class QuestionTemReviewService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 与入库、判重一致:questions_tem 行用于比对的题干(stem 优先,否则 content)
|
|
|
+ * 与中间列表 {@see listTemQuestionsForKp}(excludeFormalDuplicates=true)同口径:本 KP 下、题干未与正式库重复、且题干非空的 questions_tem 行数。
|
|
|
+ *
|
|
|
+ * @param list<string> $kpCodes
|
|
|
+ * @return array<string, int> kp_code => count
|
|
|
+ */
|
|
|
+ private function importableTemCountsForKpCodes(array $kpCodes): array
|
|
|
+ {
|
|
|
+ $kpCodes = array_values(array_unique(array_filter($kpCodes)));
|
|
|
+ if ($kpCodes === [] || ! Schema::hasTable('questions_tem')) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (! Schema::hasTable('questions')) {
|
|
|
+ return collect($kpCodes)
|
|
|
+ ->mapWithKeys(fn (string $k): array => [$k => (int) DB::table('questions_tem')->where('kp_code', $k)->count()])
|
|
|
+ ->all();
|
|
|
+ }
|
|
|
+
|
|
|
+ $placeholders = implode(',', array_fill(0, count($kpCodes), '?'));
|
|
|
+ $sql = "
|
|
|
+ SELECT t.kp_code AS kp_code, COUNT(*) AS c
|
|
|
+ FROM questions_tem t
|
|
|
+ WHERE t.kp_code IN ($placeholders)
|
|
|
+ AND t.stem IS NOT NULL AND t.stem != ''
|
|
|
+ AND NOT EXISTS (
|
|
|
+ SELECT 1 FROM questions q
|
|
|
+ WHERE q.kp_code = t.kp_code
|
|
|
+ AND q.stem != ''
|
|
|
+ AND q.stem = t.stem
|
|
|
+ )
|
|
|
+ GROUP BY t.kp_code
|
|
|
+ ";
|
|
|
+
|
|
|
+ $out = array_fill_keys($kpCodes, 0);
|
|
|
+ foreach (DB::select($sql, $kpCodes) as $row) {
|
|
|
+ $k = (string) ($row->kp_code ?? '');
|
|
|
+ if ($k !== '') {
|
|
|
+ $out[$k] = (int) ($row->c ?? 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 与入库、判重一致:questions_tem.stem(当前库表仅此 longtext 题干列,无 content)。
|
|
|
*/
|
|
|
public function normalizedStemFromTemRow(object|array $row): string
|
|
|
{
|
|
|
$arr = is_array($row) ? $row : (array) $row;
|
|
|
|
|
|
- return (string) ($arr['stem'] ?? $arr['content'] ?? '');
|
|
|
+ return (string) ($arr['stem'] ?? '');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -167,9 +216,12 @@ class QuestionTemReviewService
|
|
|
*/
|
|
|
public function buildPdfStylePreviewFields(array $row): array
|
|
|
{
|
|
|
- $stem = (string) ($row['stem'] ?? $row['content'] ?? '');
|
|
|
+ $stem = (string) ($row['stem'] ?? '');
|
|
|
$answer = (string) ($row['answer'] ?? $row['correct_answer'] ?? '');
|
|
|
- $solution = (string) ($row['solution'] ?? '');
|
|
|
+ $solution = AnswerSolutionStepMarkerInjector::enrichIfNeeded(
|
|
|
+ (string) ($row['solution'] ?? ''),
|
|
|
+ $row['question_type'] ?? $row['tags'] ?? 'answer'
|
|
|
+ );
|
|
|
$questionType = strtolower((string) ($row['question_type'] ?? $row['tags'] ?? 'answer'));
|
|
|
|
|
|
$options = $row['options'] ?? null;
|
|
|
@@ -285,6 +337,9 @@ class QuestionTemReviewService
|
|
|
? max(0.0, min(0.9, round($difficultyOverride, 2)))
|
|
|
: $this->defaultDifficultyForTemRow($arr);
|
|
|
|
|
|
+ $rawSolution = (string) ($arr['solution'] ?? '');
|
|
|
+ $solutionStored = AnswerSolutionStepMarkerInjector::enrichIfNeeded($rawSolution, $arr['question_type'] ?? $arr['tags'] ?? 'answer');
|
|
|
+
|
|
|
$payload = [
|
|
|
'question_code' => 'QT'.strtoupper(Str::random(12)),
|
|
|
'question_type' => $questionType,
|
|
|
@@ -292,7 +347,7 @@ class QuestionTemReviewService
|
|
|
'stem' => $stem,
|
|
|
'options' => $options,
|
|
|
'answer' => (string) ($arr['answer'] ?? $arr['correct_answer'] ?? ''),
|
|
|
- 'solution' => (string) ($arr['solution'] ?? ''),
|
|
|
+ 'solution' => $solutionStored,
|
|
|
'difficulty' => $difficulty,
|
|
|
'source' => 'questions_tem_review',
|
|
|
'tags' => is_string($arr['tags'] ?? null) ? $arr['tags'] : null,
|