Procházet zdrojové kódy

学情分析修复和增加教材系列状态

yemeishu před 20 hodinami
rodič
revize
cd0f39e6f7

+ 5 - 2
app/Http/Controllers/Api/ExamAnswerAnalysisController.php

@@ -40,8 +40,11 @@ class ExamAnswerAnalysisController extends Controller
                 'student_id' => 'required|string|max:255',
                 'questions' => 'required|array|min:1',
                 'questions.*.question_id' => 'required|string|max:255',
-                'questions.*.score' => 'required|numeric|min:0',
-                'questions.*.score_obtained' => 'required|numeric|min:0',
+                'questions.*.student_answer' => 'sometimes|string',
+                'questions.*.is_correct' => 'sometimes|array',
+                'questions.*.teacher_comment' => 'sometimes|string|nullable',
+                'questions.*.score' => 'sometimes|numeric|min:0',
+                'questions.*.score_obtained' => 'sometimes|numeric|min:0',
                 'questions.*.steps' => 'sometimes|array',
                 'questions.*.steps.*.step_index' => 'required_with:questions.*.steps|integer|min:1',
                 'questions.*.steps.*.is_correct' => 'required_with:questions.*.steps|boolean',

+ 51 - 2
app/Http/Controllers/Api/TextbookApiController.php

@@ -48,6 +48,16 @@ class TextbookApiController extends Controller
                 $params['status'] = $request->get('status');
             }
 
+            // 支持包含未发布的教材(默认只返回已发布的)
+            if ($request->has('include_unpublished')) {
+                $params['include_unpublished'] = $request->get('include_unpublished');
+            }
+
+            // 支持包含未启用系列的教材(默认只返回启用系列的)
+            if ($request->has('include_inactive_series')) {
+                $params['include_inactive_series'] = $request->get('include_inactive_series');
+            }
+
             $result = $this->textbookService->getTextbooks($params);
 
             // 格式化返回数据
@@ -82,11 +92,20 @@ class TextbookApiController extends Controller
             return response()->json([
                 'success' => true,
                 'data' => $textbooks,
-                'meta' => $result['meta'] ?? [
+                'meta' => array_merge($result['meta'] ?? [], [
                     'page' => $params['page'],
                     'per_page' => $params['per_page'],
                     'total' => count($textbooks),
-                ]
+                    'filters' => [
+                        'grade' => $request->get('grade'),
+                        'stage' => $request->get('stage'),
+                        'semester' => $request->get('semester'),
+                        'series_id' => $request->get('series_id'),
+                        'status' => $request->get('status'),
+                        'include_unpublished' => $request->get('include_unpublished') === 'true',
+                        'include_inactive_series' => $request->get('include_inactive_series') === 'true',
+                    ]
+                ])
             ]);
 
         } catch (\Exception $e) {
@@ -129,6 +148,19 @@ class TextbookApiController extends Controller
             if ($request->has('series_id')) {
                 $params['series_id'] = $request->get('series_id');
             }
+            if ($request->has('status')) {
+                $params['status'] = $request->get('status');
+            }
+
+            // 支持包含未发布的教材(默认只返回已发布的)
+            if ($request->has('include_unpublished')) {
+                $params['include_unpublished'] = $request->get('include_unpublished');
+            }
+
+            // 支持包含未启用系列的教材(默认只返回启用系列的)
+            if ($request->has('include_inactive_series')) {
+                $params['include_inactive_series'] = $request->get('include_inactive_series');
+            }
 
             $result = $this->textbookService->getTextbooks($params);
             $textbooks = $this->formatTextbookList($result['data'] ?? []);
@@ -146,6 +178,13 @@ class TextbookApiController extends Controller
                     'stage' => $params['stage'] ?? null,
                     'stage_label' => isset($params['stage']) ? $this->getStageLabel($params['stage']) : null,
                     'total' => count($textbooks),
+                    'filters' => [
+                        'semester' => $request->get('semester'),
+                        'series_id' => $request->get('series_id'),
+                        'status' => $request->get('status'),
+                        'include_unpublished' => $request->get('include_unpublished') === 'true',
+                        'include_inactive_series' => $request->get('include_inactive_series') === 'true',
+                    ]
                 ]
             ]);
 
@@ -209,10 +248,16 @@ class TextbookApiController extends Controller
         try {
             $params = [];
 
+            // 支持按学段筛选
             if ($request->has('stage')) {
                 $params['stage'] = $this->convertStageToCode($request->get('stage'));
             }
 
+            // 支持包含未启用的系列(默认只返回启用的)
+            if ($request->has('include_inactive')) {
+                $params['include_inactive'] = $request->get('include_inactive');
+            }
+
             $result = $this->textbookService->getTextbookSeries($params);
 
             $series = array_map(function ($item) {
@@ -236,6 +281,10 @@ class TextbookApiController extends Controller
                 'data' => $series,
                 'meta' => [
                     'total' => count($series),
+                    'filters' => [
+                        'stage' => $request->get('stage'),
+                        'include_inactive' => $request->get('include_inactive') === 'true',
+                    ]
                 ]
             ]);
 

+ 96 - 0
app/Services/ExamAnswerAnalysisService.php

@@ -58,6 +58,9 @@ class ExamAnswerAnalysisService
         $studentId = $examData['student_id'];
         $questions = $examData['questions'] ?? [];
 
+        // 0. 自动计算分数(如果用户未提供)
+        $questions = $this->autoCalculateScores($questions);
+
         // 1. 获取学案基准难度
         $examBaseDifficulty = $this->getExamBaseDifficulty($examData['paper_id'] ?? '');
 
@@ -981,4 +984,97 @@ class ExamAnswerAnalysisService
             return 'challenge'; // 挑战型
         }
     }
+
+    /**
+     * 自动计算题目分数
+     * 如果用户未提供 score 和 score_obtained,则根据 is_correct 字段自动计算
+     *
+     * @param array $questions 题目列表
+     * @return array 处理后的题目列表
+     */
+    private function autoCalculateScores(array $questions): array
+    {
+        foreach ($questions as &$question) {
+            // 如果用户没有提供 score,尝试从数据库获取或使用默认值
+            if (!isset($question['score'])) {
+                $question['score'] = $this->getQuestionDefaultScore($question['question_id'] ?? '');
+            }
+
+            // 如果用户没有提供 score_obtained,根据 is_correct 计算
+            if (!isset($question['score_obtained'])) {
+                $question['score_obtained'] = $this->calculateScoreObtained(
+                    $question['score'] ?? 0,
+                    $question['is_correct'] ?? []
+                );
+            }
+
+            Log::debug('自动计算分数', [
+                'question_id' => $question['question_id'],
+                'default_score' => $question['score'],
+                'score_obtained' => $question['score_obtained'],
+                'is_correct' => $question['is_correct'] ?? []
+            ]);
+        }
+
+        return $questions;
+    }
+
+    /**
+     * 获取题目默认分数
+     */
+    private function getQuestionDefaultScore(string $questionId): float
+    {
+        if (empty($questionId)) {
+            return 2.0; // 默认分数
+        }
+
+        try {
+            // 尝试从题库获取题目分数
+            $question = DB::connection('mysql')
+                ->table('questions')
+                ->where('id', $questionId)
+                ->orWhere('question_code', $questionId)
+                ->first();
+
+            if ($question && isset($question->score)) {
+                return (float) $question->score;
+            }
+
+            // 如果没有找到,根据题目ID生成一个合理的默认分数
+            // 这里可以根据需要调整默认分数逻辑
+            return 2.0;
+
+        } catch (\Exception $e) {
+            Log::warning('获取题目默认分数失败,使用默认值', [
+                'question_id' => $questionId,
+                'error' => $e->getMessage()
+            ]);
+            return 2.0;
+        }
+    }
+
+    /**
+     * 根据 is_correct 数组计算得分
+     *
+     * @param float $totalScore 总分
+     * @param array $isCorrect 正确性数组 [0, 1, 1] 表示第1题错误,第2、3题正确
+     * @return float 得分
+     */
+    private function calculateScoreObtained(float $totalScore, array $isCorrect): float
+    {
+        if (empty($isCorrect)) {
+            return 0.0;
+        }
+
+        $correctCount = array_sum($isCorrect);
+        $totalCount = count($isCorrect);
+
+        if ($totalCount === 0) {
+            return 0.0;
+        }
+
+        // 按正确率计算得分
+        $scoreRatio = $correctCount / $totalCount;
+        return round($totalScore * $scoreRatio, 2);
+    }
 }

+ 60 - 8
app/Services/TextbookApiService.php

@@ -83,7 +83,20 @@ class TextbookApiService
     public function getTextbookSeries(array $params = []): array
     {
         if ($this->useDatabase) {
-            $series = TextbookSeries::query()->orderBy('sort_order')->get();
+            $query = TextbookSeries::query()->orderBy('sort_order');
+
+            // 默认只返回启用的系列(is_active = true)
+            // 除非显式传入 include_inactive=true
+            if (!isset($params['include_inactive']) || $params['include_inactive'] !== 'true') {
+                $query->where('is_active', true);
+            }
+
+            // 支持按学段筛选
+            if (isset($params['stage'])) {
+                $query->where('stages', 'like', '%' . $params['stage'] . '%');
+            }
+
+            $series = $query->get();
             return ['data' => $series->toArray(), 'meta' => ['total' => $series->count()]];
         }
 
@@ -203,23 +216,46 @@ class TextbookApiService
     public function getTextbooks(array $params = []): array
     {
         if ($this->useDatabase) {
-            $query = Textbook::query();
+            $query = Textbook::query()
+                ->select('textbooks.*')
+                ->join('textbook_series', 'textbooks.series_id', '=', 'textbook_series.id');
+
+            // 默认只返回已发布且系列启用的教材
+            // 除非显式传入 include_unpublished=true 或 include_inactive_series=true
+            if (!isset($params['include_unpublished']) || $params['include_unpublished'] !== 'true') {
+                $query->where('textbooks.status', 'published');
+            }
+
+            if (!isset($params['include_inactive_series']) || $params['include_inactive_series'] !== 'true') {
+                $query->where('textbook_series.is_active', true);
+            }
+
+            // 支持按系列ID筛选
             if (isset($params['series_id'])) {
-                $query->where('series_id', $params['series_id']);
+                $query->where('textbooks.series_id', $params['series_id']);
             }
+
+            // 支持按学段筛选
             if (isset($params['stage'])) {
-                $query->where('stage', $params['stage']);
+                $query->where('textbooks.stage', $params['stage']);
             }
+
+            // 支持按年级筛选
             if (isset($params['grade'])) {
-                $query->where('grade', $params['grade']);
+                $query->where('textbooks.grade', $params['grade']);
             }
+
+            // 支持按学期筛选
             if (array_key_exists('semester', $params) && $params['semester'] !== null) {
-                $query->where('semester', $params['semester']);
+                $query->where('textbooks.semester', $params['semester']);
             }
+
+            // 显式按状态筛选(会覆盖默认的已发布筛选)
             if (isset($params['status'])) {
-                $query->where('status', $params['status']);
+                $query->where('textbooks.status', $params['status']);
             }
-            $textbooks = $query->orderBy('id')->get();
+
+            $textbooks = $query->orderBy('textbooks.id')->get();
             return ['data' => $textbooks->toArray(), 'meta' => ['total' => $textbooks->count()]];
         }
 
@@ -345,6 +381,22 @@ class TextbookApiService
     public function getTextbookCatalog(int $textbookId, string $format = 'tree'): array
     {
         if ($this->useDatabase) {
+            // 先验证教材是否存在且已发布,且其系列处于启用状态
+            $textbook = Textbook::query()
+                ->select('textbooks.*')
+                ->join('textbook_series', 'textbooks.series_id', '=', 'textbook_series.id')
+                ->where('textbooks.id', $textbookId)
+                ->where('textbooks.status', 'published')
+                ->where('textbook_series.is_active', true)
+                ->first();
+
+            if (!$textbook) {
+                Log::warning('Textbook not found or not published or series inactive', [
+                    'textbook_id' => $textbookId
+                ]);
+                return [];
+            }
+
             $nodes = TextbookCatalog::query()
                 ->where('textbook_id', $textbookId)
                 ->orderBy('sort_order')