|
|
@@ -163,17 +163,22 @@ class IntelligentExamController extends Controller
|
|
|
], 400);
|
|
|
}
|
|
|
|
|
|
- $totalScore = array_sum(array_column($questions, 'score'));
|
|
|
$totalQuestions = min($data['total_questions'], count($questions));
|
|
|
$questions = array_slice($questions, 0, $totalQuestions);
|
|
|
|
|
|
+ // 调整题目分值,确保符合中国中学卷子标准(总分100分)
|
|
|
+ $questions = $this->adjustQuestionScores($questions, 100.0);
|
|
|
+
|
|
|
+ // 计算总分
|
|
|
+ $totalScore = array_sum(array_column($questions, 'score'));
|
|
|
+
|
|
|
// 第二步:保存试卷到数据库(同步)
|
|
|
$paperId = $this->questionBankService->saveExamToDatabase([
|
|
|
'paper_name' => $paperName,
|
|
|
'student_id' => $data['student_id'],
|
|
|
'teacher_id' => $data['teacher_id'] ?? null,
|
|
|
'difficulty_category' => $difficultyCategory,
|
|
|
- 'total_score' => $data['total_score'] ?? $totalScore,
|
|
|
+ 'total_score' => $data['total_score'] ?? 100.0, // 默认100分
|
|
|
'questions' => $questions,
|
|
|
]);
|
|
|
|
|
|
@@ -235,9 +240,21 @@ class IntelligentExamController extends Controller
|
|
|
'trace' => $e->getTraceAsString(),
|
|
|
]);
|
|
|
|
|
|
+ // 返回更具体的错误信息
|
|
|
+ $errorMessage = $e->getMessage();
|
|
|
+ if (strpos($errorMessage, 'Connection') !== false || strpos($errorMessage, 'connection') !== false) {
|
|
|
+ $errorMessage = '依赖服务连接失败,请检查服务状态';
|
|
|
+ } elseif (strpos($errorMessage, 'timeout') !== false || strpos($errorMessage, '超时') !== false) {
|
|
|
+ $errorMessage = '服务响应超时,请稍后重试';
|
|
|
+ } elseif (strpos($errorMessage, 'not found') !== false || strpos($errorMessage, '未找到') !== false) {
|
|
|
+ $errorMessage = '请求的资源不存在';
|
|
|
+ } elseif (strpos($errorMessage, 'invalid') !== false || strpos($errorMessage, '无效') !== false) {
|
|
|
+ $errorMessage = '请求参数无效';
|
|
|
+ }
|
|
|
+
|
|
|
return response()->json([
|
|
|
'success' => false,
|
|
|
- 'message' => '服务异常,请稍后重试',
|
|
|
+ 'message' => $errorMessage ?: '服务异常,请稍后重试',
|
|
|
], 500);
|
|
|
}
|
|
|
}
|
|
|
@@ -360,6 +377,12 @@ class IntelligentExamController extends Controller
|
|
|
*/
|
|
|
private function normalizePayload(array $payload): array
|
|
|
{
|
|
|
+ // 处理 question_count 参数:转换为 total_questions
|
|
|
+ if (isset($payload['question_count']) && !isset($payload['total_questions'])) {
|
|
|
+ $payload['total_questions'] = $payload['question_count'];
|
|
|
+ unset($payload['question_count']);
|
|
|
+ }
|
|
|
+
|
|
|
// 处理 kp_codes:空字符串或null转换为空数组
|
|
|
if (isset($payload['kp_codes'])) {
|
|
|
if (is_string($payload['kp_codes'])) {
|
|
|
@@ -529,13 +552,194 @@ class IntelligentExamController extends Controller
|
|
|
return '解答题';
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据题目类型获取默认分值(中国中学卷子标准)
|
|
|
+ * 选择题:5分/题,填空题:5分/题,解答题:10分/题
|
|
|
+ */
|
|
|
private function defaultScore(string $type): int
|
|
|
{
|
|
|
- if ($type === '选择题' || $type === '填空题') {
|
|
|
- return 5;
|
|
|
+ return match ($type) {
|
|
|
+ '选择题' => 5,
|
|
|
+ '填空题' => 5,
|
|
|
+ '解答题' => 10,
|
|
|
+ default => 5,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算试卷总分并调整各题目分值,确保总分接近目标分数
|
|
|
+ * 符合中国中学卷子标准:
|
|
|
+ * - 选择题:约40%总分(每题4-6分,整数分值)
|
|
|
+ * - 填空题:约25%总分(每题4-6分,整数分值)
|
|
|
+ * - 解答题:约35%总分(每题8-12分,整数分值)
|
|
|
+ * 使用组合优化算法确保:
|
|
|
+ * 1. 所有分值都是整数(无小数点)
|
|
|
+ * 2. 同类型题目分值均匀
|
|
|
+ * 3. 总分精确匹配目标分数(或最接近)
|
|
|
+ */
|
|
|
+ private function adjustQuestionScores(array $questions, float $targetTotalScore = 100.0): array
|
|
|
+ {
|
|
|
+ if (empty($questions)) {
|
|
|
+ return $questions;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 统计各类型题目数量
|
|
|
+ $typeCounts = ['choice' => 0, 'fill' => 0, 'answer' => 0];
|
|
|
+
|
|
|
+ foreach ($questions as $question) {
|
|
|
+ $type = $question['question_type'] ?? 'answer';
|
|
|
+ if (in_array($type, ['CHOICE', 'SINGLE_CHOICE', 'MULTIPLE_CHOICE'], true)) {
|
|
|
+ $type = 'choice';
|
|
|
+ } elseif (in_array($type, ['FILL_IN_THE_BLANK', 'FILL'], true)) {
|
|
|
+ $type = 'fill';
|
|
|
+ } elseif (in_array($type, ['CALCULATION', 'WORD_PROBLEM', 'PROOF', 'ANSWER'], true)) {
|
|
|
+ $type = 'answer';
|
|
|
+ }
|
|
|
+ if (isset($typeCounts[$type])) {
|
|
|
+ $typeCounts[$type]++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标准分值范围
|
|
|
+ $standardScoreRanges = [
|
|
|
+ 'choice' => ['min' => 4, 'max' => 6],
|
|
|
+ 'fill' => ['min' => 4, 'max' => 6],
|
|
|
+ 'answer' => ['min' => 8, 'max' => 12],
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 目标比例
|
|
|
+ $typeRatios = ['choice' => 0.40, 'fill' => 0.25, 'answer' => 0.35];
|
|
|
+
|
|
|
+ // 检查可用题型
|
|
|
+ $availableTypes = array_filter($typeCounts, fn($count) => $count > 0);
|
|
|
+ $availableTypeCount = count($availableTypes);
|
|
|
+ $isPartialTypes = $availableTypeCount < 3 && $availableTypeCount > 0;
|
|
|
+
|
|
|
+ if ($isPartialTypes) {
|
|
|
+ $equalRatio = 1.0 / $availableTypeCount;
|
|
|
+ foreach ($typeCounts as $type => $count) {
|
|
|
+ if ($count > 0) {
|
|
|
+ $typeRatios[$type] = $equalRatio;
|
|
|
+ } else {
|
|
|
+ $typeRatios[$type] = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $typeQuestionIndexes = ['choice' => [], 'fill' => [], 'answer' => []];
|
|
|
+
|
|
|
+ // 记录每种题型的题目索引
|
|
|
+ foreach ($questions as $index => $question) {
|
|
|
+ $type = $question['question_type'] ?? 'answer';
|
|
|
+ if (in_array($type, ['CHOICE', 'SINGLE_CHOICE', 'MULTIPLE_CHOICE'], true)) {
|
|
|
+ $type = 'choice';
|
|
|
+ } elseif (in_array($type, ['FILL_IN_THE_BLANK', 'FILL'], true)) {
|
|
|
+ $type = 'fill';
|
|
|
+ } elseif (in_array($type, ['CALCULATION', 'WORD_PROBLEM', 'PROOF', 'ANSWER'], true)) {
|
|
|
+ $type = 'answer';
|
|
|
+ }
|
|
|
+ $typeQuestionIndexes[$type][] = $index;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成每种题型的可能分值选项
|
|
|
+ $typeScoreOptions = [];
|
|
|
+ foreach ($typeQuestionIndexes as $type => $indexes) {
|
|
|
+ if (empty($indexes)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $typeQuestionCount = count($indexes);
|
|
|
+ $minScore = $standardScoreRanges[$type]['min'];
|
|
|
+ $maxScore = $standardScoreRanges[$type]['max'];
|
|
|
+ $targetTotal = $targetTotalScore * $typeRatios[$type];
|
|
|
+ $idealPerQuestion = $targetTotal / $typeQuestionCount;
|
|
|
+
|
|
|
+ $options = [];
|
|
|
+
|
|
|
+ // 添加标准范围内的选项
|
|
|
+ for ($score = $minScore; $score <= $maxScore; $score++) {
|
|
|
+ $total = $score * $typeQuestionCount;
|
|
|
+ $options[] = [
|
|
|
+ 'score' => $score,
|
|
|
+ 'total' => $total,
|
|
|
+ 'difference' => abs($targetTotalScore - $total),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是部分题型,大幅扩展搜索范围
|
|
|
+ if ($isPartialTypes) {
|
|
|
+ $idealScore = (int) round($idealPerQuestion);
|
|
|
+ $searchMin = max($minScore, $idealScore - 10);
|
|
|
+ $searchMax = $idealScore + 10;
|
|
|
+
|
|
|
+ for ($score = $searchMin; $score <= $searchMax; $score++) {
|
|
|
+ if ($score >= $minScore) {
|
|
|
+ $total = $score * $typeQuestionCount;
|
|
|
+ if (!in_array($total, array_column($options, 'total'))) {
|
|
|
+ $options[] = [
|
|
|
+ 'score' => $score,
|
|
|
+ 'total' => $total,
|
|
|
+ 'difference' => abs($targetTotalScore - $total),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $typeScoreOptions[$type] = $options;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成所有可能的组合
|
|
|
+ $types = array_keys(array_filter($typeQuestionIndexes, fn($indexes) => !empty($indexes)));
|
|
|
+ $allCombinations = [[]];
|
|
|
+
|
|
|
+ foreach ($types as $type) {
|
|
|
+ $newCombinations = [];
|
|
|
+ foreach ($allCombinations as $combo) {
|
|
|
+ foreach ($typeScoreOptions[$type] as $option) {
|
|
|
+ $newCombo = $combo;
|
|
|
+ $newCombo[$type] = $option;
|
|
|
+ $newCombinations[] = $newCombo;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $allCombinations = $newCombinations;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 找到最佳组合(优先精确匹配,其次最接近)
|
|
|
+ $bestCombination = null;
|
|
|
+ $bestDifference = PHP_FLOAT_MAX;
|
|
|
+ $exactMatchFound = false;
|
|
|
+
|
|
|
+ foreach ($allCombinations as $combo) {
|
|
|
+ $totalScore = array_sum(array_column($combo, 'total'));
|
|
|
+ $difference = abs($targetTotalScore - $totalScore);
|
|
|
+
|
|
|
+ if ($difference == 0) {
|
|
|
+ $bestCombination = $combo;
|
|
|
+ $exactMatchFound = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($difference < $bestDifference) {
|
|
|
+ $bestDifference = $difference;
|
|
|
+ $bestCombination = $combo;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应用最佳组合
|
|
|
+ $adjustedQuestions = [];
|
|
|
+ if ($bestCombination) {
|
|
|
+ foreach ($bestCombination as $type => $option) {
|
|
|
+ $score = $option['score'];
|
|
|
+ foreach ($typeQuestionIndexes[$type] as $index) {
|
|
|
+ $question = $questions[$index];
|
|
|
+ $question['score'] = $score;
|
|
|
+ $adjustedQuestions[$index] = $question;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return 10;
|
|
|
+ return array_values($adjustedQuestions);
|
|
|
}
|
|
|
|
|
|
private function resolveMistakeQuestionIds(string $studentId, array $mistakeIds, array $mistakeQuestionIds): array
|