|
@@ -62,9 +62,36 @@ class AssembleExamTaskJob implements ShouldQueue
|
|
|
$diagnosticChapterId = null;
|
|
$diagnosticChapterId = null;
|
|
|
$explanationKpCodes = null;
|
|
$explanationKpCodes = null;
|
|
|
|
|
|
|
|
- if (! empty($mistakeIds) || ! empty($mistakeQuestionIds)) {
|
|
|
|
|
- // assemble_type=5(按卷追练):显式传错题时必须校验 mistake_records 归属该学生,再组卷;
|
|
|
|
|
- // 其它 assemble_type 保持原样(仅过滤能得到则拉题,不做全量校验)。
|
|
|
|
|
|
|
+ if ($assembleType === 15) {
|
|
|
|
|
+ // 错题本组卷:paper_ids 传题库题目 question_id(须在该学生错题本 mistake_records 中存在),与 assemble_type=5 按卷追练(真实试卷 paper_id)分离
|
|
|
|
|
+ $questionIdList = $this->normalizeBankQuestionIdsList($paperIds);
|
|
|
|
|
+ if ($questionIdList === []) {
|
|
|
|
|
+ $taskManager->markTaskFailed($this->taskId, '错题本组卷需提供 paper_ids(题库题目 id)');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $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 = $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 ($assembleType === 5) {
|
|
if ($assembleType === 5) {
|
|
|
$strict = $this->resolveMistakeQuestionIdsStrictForStudent(
|
|
$strict = $this->resolveMistakeQuestionIdsStrictForStudent(
|
|
|
(string) $data['student_id'],
|
|
(string) $data['student_id'],
|
|
@@ -161,7 +188,7 @@ class AssembleExamTaskJob implements ShouldQueue
|
|
|
|
|
|
|
|
$finalStats = $result['stats'] ?? [
|
|
$finalStats = $result['stats'] ?? [
|
|
|
'total_selected' => count($questions),
|
|
'total_selected' => count($questions),
|
|
|
- 'mistake_based' => ! empty($mistakeIds) || ! empty($mistakeQuestionIds),
|
|
|
|
|
|
|
+ 'mistake_based' => ! empty($mistakeIds) || ! empty($mistakeQuestionIds) || $assembleType === 15,
|
|
|
];
|
|
];
|
|
|
if (! isset($finalStats['difficulty_category'])) {
|
|
if (! isset($finalStats['difficulty_category'])) {
|
|
|
$finalStats['difficulty_category'] = $difficultyCategory;
|
|
$finalStats['difficulty_category'] = $difficultyCategory;
|
|
@@ -247,6 +274,7 @@ class AssembleExamTaskJob implements ShouldQueue
|
|
|
/**
|
|
/**
|
|
|
* 追练(assemble_type=5)+ 指定错题:mistake_ids 须逐条命中该学生的 mistake_records;
|
|
* 追练(assemble_type=5)+ 指定错题:mistake_ids 须逐条命中该学生的 mistake_records;
|
|
|
* mistake_question_ids 须在该学生错题本中至少有一条记录。顺序:先按 mistake_ids 请求顺序,再追加题号列表(去重)。
|
|
* mistake_question_ids 须在该学生错题本中至少有一条记录。顺序:先按 mistake_ids 请求顺序,再追加题号列表(去重)。
|
|
|
|
|
+ * 错题本组卷(assemble_type=15)将 paper_ids 解析为题库题目 id 后,仅使用本方法的 mistake_question_ids 分支做校验。
|
|
|
*
|
|
*
|
|
|
* @return array{ok: bool, message?: string, question_ids?: array<int, string>}
|
|
* @return array{ok: bool, message?: string, question_ids?: array<int, string>}
|
|
|
*/
|
|
*/
|
|
@@ -304,6 +332,47 @@ class AssembleExamTaskJob implements ShouldQueue
|
|
|
return ['ok' => true, 'question_ids' => $orderedQuestionIds];
|
|
return ['ok' => true, 'question_ids' => $orderedQuestionIds];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * assemble_type=15 时 paper_ids 承载题库题目 id:纯数字字符串转为 int,去重并保持首次出现顺序。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return array<int, int|string>
|
|
|
|
|
+ */
|
|
|
|
|
+ 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
|
|
private function hydrateQuestions(array $questions, array $kpCodes): array
|
|
|
{
|
|
{
|
|
|
$normalized = [];
|
|
$normalized = [];
|