Explorar o código

修改 api 路径的问题

yemeishu hai 3 semanas
pai
achega
9f5d8dcdd9

+ 33 - 813
app/Filament/Pages/UploadExamPaper.php

@@ -2,24 +2,20 @@
 
 namespace App\Filament\Pages;
 
-use App\Jobs\ProcessOCRRecord;
-use App\Models\OCRRecord;
 use App\Models\Student;
 use App\Models\Teacher;
+use App\Services\ExamPaperService;
 use App\Filament\Traits\HasUserRole;
 use BackedEnum;
 use Filament\Notifications\Notification;
 use Filament\Pages\Page;
-use Filament\Forms;
-use Livewire\WithFileUploads;
 use Livewire\Attributes\Computed;
 use Livewire\Attributes\On;
-use Illuminate\Support\Facades\Storage;
 use UnitEnum;
 
 class UploadExamPaper extends Page
 {
-    use HasUserRole, WithFileUploads;
+    use HasUserRole;
 
     protected static ?string $title = '上传试卷';
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cloud-arrow-up';
@@ -31,24 +27,12 @@ class UploadExamPaper extends Page
 
     public ?string $teacherId = null;
     public ?string $studentId = null;
-    public $uploadedImage = null;
-    public bool $isUploading = false;
-    public ?string $paperType = null; // 试卷类型:unit_test, midterm, final, homework, quiz, other
-    public $form;
-    public array $data = [];
-    public bool $analyzing = false;
-    public ?string $analysisError = null;
-
-    // 新增:模式选择
+
+    // 模式选择
     public string $mode = 'upload'; // 'upload' 或 'select_paper'
     public ?string $selectedPaperId = null;
-    public bool $showGrading = false;
     public array $questions = [];
     public array $gradingData = [];
-    public ?string $paperName = null;
-    public ?string $paperClass = null;
-    public ?string $paperStudent = null;
-    public ?string $paperDate = null;
     public array $questionGrades = []; // 存储每道题的评分
 
     public function mount()
@@ -67,411 +51,41 @@ class UploadExamPaper extends Page
         }
 
         $this->studentId = null;
-        $this->uploadedImage = null;
-        $this->paperType = null;
         $this->mode = 'upload';
         $this->selectedPaperId = null;
         $this->questionGrades = [];
     }
 
-    public function form(Forms\Form $form): Forms\Form
-    {
-        return $form
-            ->statePath('data')
-            ->schema([
-                Forms\Components\FileUpload::make('image')
-                    ->label('上传试卷图片')
-                    ->image()
-                    ->multiple()
-                    ->directory('exam-papers')
-                    ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/jpg'])
-                    ->helperText('支持PNG、JPG、JPEG格式,可同时上传多张图片')
-                    ->maxFiles(10)
-                    ->required(),
-
-                Forms\Components\TextInput::make('paper_name')
-                    ->label('试卷名称')
-                    ->required()
-                    ->placeholder('例如:数学期末考试'),
-
-                Forms\Components\Select::make('class')
-                    ->label('班级')
-                    ->options([
-                        'ClassA' => '三年级一班',
-                        'ClassB' => '三年级二班',
-                        'ClassC' => '四年级一班',
-                        'ClassD' => '四年级二班',
-                        'ClassE' => '五年级一班',
-                        'ClassF' => '五年级二班',
-                        'ClassG' => '六年级一班',
-                        'ClassH' => '六年级二班',
-                    ])
-                    ->required(),
-
-                Forms\Components\TextInput::make('student_name')
-                    ->label('学生姓名')
-                    ->required()
-                    ->placeholder('请输入学生姓名'),
-
-                Forms\Components\Select::make('paper_type')
-                    ->label('试卷类型')
-                    ->options([
-                        'quiz' => '课堂测验',
-                        'midterm' => '期中考试',
-                        'final' => '期末考试',
-                        'homework' => '家庭作业',
-                    ])
-                    ->default('quiz')
-                    ->required(),
-
-                Forms\Components\TextInput::make('paper_subject')
-                    ->label('科目')
-                    ->default('数学')
-                    ->required(),
-            ]);
-    }
-
     #[Computed]
     public function teachers(): array
     {
-        try {
-            $query = Teacher::query()
-                ->leftJoin('users as u', 'teachers.teacher_id', '=', 'u.user_id')
-                ->select(
-                    'teachers.teacher_id',
-                    'teachers.name',
-                    'teachers.subject',
-                    'u.username',
-                    'u.email'
-                );
-
-            // 如果是老师,只返回自己
-            if ($this->isTeacher) {
-                $teacherId = $this->getCurrentTeacherId();
-                if ($teacherId) {
-                    $query->where('teachers.teacher_id', $teacherId);
-                }
-            }
-
-            $teachers = $query->orderBy('teachers.name')->get();
-
-            // 检查是否有学生没有对应的老师记录
-            $teacherIds = $teachers->pluck('teacher_id')->toArray();
-            $missingTeacherIds = Student::query()
-                ->distinct()
-                ->whereNotIn('teacher_id', $teacherIds)
-                ->pluck('teacher_id')
-                ->toArray();
-
-            $teachersArray = $teachers->all();
-
-            if (!empty($missingTeacherIds)) {
-                foreach ($missingTeacherIds as $missingId) {
-                    $teachersArray[] = (object) [
-                        'teacher_id' => $missingId,
-                        'name' => '未知老师 (' . $missingId . ')',
-                        'subject' => '未知',
-                        'username' => null,
-                        'email' => null
-                    ];
-                }
-
-                usort($teachersArray, function($a, $b) {
-                    return strcmp($a->name, $b->name);
-                });
-            }
-
-            return $teachersArray;
-        } catch (\Exception $e) {
-            \Illuminate\Support\Facades\Log::error('加载老师列表失败', [
-                'error' => $e->getMessage()
-            ]);
-            return [];
-        }
+        return app(ExamPaperService::class)->getTeachers(
+            $this->isTeacher ? $this->getCurrentTeacherId() : null
+        );
     }
 
     #[Computed]
     public function students(): array
     {
-        if (empty($this->teacherId)) {
-            return [];
-        }
-
-        try {
-            return Student::query()
-                ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id')
-                ->where('students.teacher_id', $this->teacherId)
-                ->select(
-                    'students.student_id',
-                    'students.name',
-                    'students.grade',
-                    'students.class_name',
-                    'u.username',
-                    'u.email'
-                )
-                ->orderBy('students.grade')
-                ->orderBy('students.class_name')
-                ->orderBy('students.name')
-                ->get()
-                ->all();
-        } catch (\Exception $e) {
-            \Illuminate\Support\Facades\Log::error('加载学生列表失败', [
-                'teacher_id' => $this->teacherId,
-                'error' => $e->getMessage()
-            ]);
-            return [];
-        }
+        return app(ExamPaperService::class)->getStudents($this->teacherId);
     }
 
     #[Computed]
     public function recentRecords(): array
     {
-        // 1. 获取OCR记录(图片上传)
-        $ocrQuery = OCRRecord::with('student');
-
-        // 如果选择了学生,则筛选该学生的记录
-        if (!empty($this->studentId)) {
-            $ocrQuery->where('user_id', $this->studentId);
-        }
-
-        $ocrRecords = $ocrQuery->latest()->take(5)->get()
-            ->map(function($record) {
-                $studentName = $record->student?->name ?: ('学生ID: ' . $record->user_id);
-
-                return [
-                    'type' => 'ocr_upload',
-                    'id' => $record->id,
-                    'record_id' => $record->id,
-                    'paper_id' => null,
-                    'student_id' => $record->user_id,
-                    'student_name' => $studentName,
-                    'paper_type' => $record->paper_type_label,
-                    'paper_name' => $record->image_filename ?: '未命名图片',
-                    'status' => $record->status,
-                    'total_questions' => $record->total_questions,
-                    'processed_questions' => $record->processed_questions ?? 0,
-                    'created_at' => $record->created_at->format('Y-m-d H:i'),
-                    'is_completed' => $record->status === 'completed',
-                ];
-            })->toArray();
-
-        // 2. 获取所有Paper记录(包括草稿和已评分)
-        $paperQuery = \App\Models\Paper::with('student');
-
-        // 如果选择了学生,则筛选该学生的记录
-        if (!empty($this->studentId)) {
-            $paperQuery->where('student_id', $this->studentId);
-        }
-
-        $allPapers = $paperQuery->latest()->take(5)->get()
-            ->map(function($paper) {
-                $type = $paper->status === 'completed' ? 'graded_paper' : 'generated';
-                $paperType = $paper->status === 'completed' ? '已评分试卷' : '系统生成试卷';
-                $iconColor = $paper->status === 'completed' ? 'text-green-500' : 'text-blue-500';
-
-                $studentName = $paper->student?->name ?: ('学生ID: ' . $paper->student_id);
-
-                return [
-                    'type' => $type,
-                    'id' => $paper->paper_id,
-                    'record_id' => null,
-                    'paper_id' => $paper->paper_id,
-                    'student_id' => $paper->student_id,
-                    'student_name' => $studentName,
-                    'paper_type' => $paperType,
-                    'paper_name' => $paper->paper_name ?? '未命名试卷',
-                    'status' => $paper->difficulty_category,
-                    'total_questions' => $paper->question_count ?? 0,
-                    'created_at' => $paper->created_at->format('Y-m-d H:i'),
-                    'is_completed' => $paper->status === 'completed',
-                    'icon_color' => $iconColor,
-                ];
-            })->toArray();
-
-        // 3. 合并并按时间排序
-        $allRecords = array_merge($ocrRecords, $allPapers);
-        usort($allRecords, function($a, $b) {
-            return strcmp($b['created_at'], $a['created_at']);
-        });
-
-        return array_slice($allRecords, 0, 10);
-    }
-    
-    /**
-     * 获取学生的试卷列表
-     */
-    #[Computed]
-    public function studentPapers(): array
-    {
-        if (empty($this->studentId)) {
-            return [];
-        }
-
-        try {
-            // 使用 Student 关联查询试卷
-            $student = \App\Models\Student::find($this->studentId);
-            if (!$student) {
-                \Log::warning('未找到指定学生', ['student_id' => $this->studentId]);
-                return [];
-            }
-
-            return $student->papers()
-                ->withCount('questions') // 添加题目计数
-                ->orderBy('created_at', 'desc')
-                ->take(20)
-                ->get()
-                ->map(function($paper) {
-                    return [
-                        'paper_id' => $paper->paper_id, // 使用 paper_id 而不是 id
-                        'paper_name' => $paper->paper_name ?? '未命名试卷',
-                        'total_questions' => $paper->questions_count ?? 0,
-                        'total_score' => $paper->total_score ?? 0,
-                        'created_at' => $paper->created_at->format('Y-m-d H:i'),
-                    ];
-                })
-                ->toArray();
-        } catch (\Exception $e) {
-            \Log::error('获取学生试卷列表失败', [
-                'student_id' => $this->studentId,
-                'error' => $e->getMessage()
-            ]);
-            return [];
-        }
+        return app(ExamPaperService::class)->getRecentRecords($this->studentId);
     }
 
     #[Computed]
-    public function paperTypes(): array
+    public function studentPapers(): array
     {
-        return [
-            '' => '请选择试卷形式',
-            'unit_test' => '单元测试',
-            'midterm' => '期中考试',
-            'final' => '期末考试',
-            'homework' => '家庭作业',
-            'quiz' => '随堂测验',
-            'other' => '其他',
-        ];
+        return app(ExamPaperService::class)->getStudentPapers($this->studentId);
     }
 
-    /**
-     * 获取选中试卷的题目列表
-     */
     #[Computed]
     public function selectedPaperQuestions(): array
     {
-        if (empty($this->selectedPaperId)) {
-            return [];
-        }
-
-        try {
-            // 首先检查试卷是否存在
-            $paper = \App\Models\Paper::where('paper_id', $this->selectedPaperId)->first();
-            if (!$paper) {
-                \Log::warning('未找到指定试卷', ['paper_id' => $this->selectedPaperId]);
-                return [];
-            }
-
-            // 使用关联关系查询题目
-            $paperWithQuestions = \App\Models\Paper::with(['questions' => function($query) {
-                $query->orderBy('question_number');
-            }])->where('paper_id', $this->selectedPaperId)->first();
-
-            $questions = $paperWithQuestions ? $paperWithQuestions->questions : collect([]);
-
-            // 处理数据不一致的情况:如果题目为空但试卷显示有题目
-            if ($questions->isEmpty() && ($paper->question_count ?? 0) > 0) {
-                \Log::warning('试卷显示有题目但实际题目数据缺失', [
-                    'paper_id' => $this->selectedPaperId,
-                    'expected_questions' => $paper->question_count,
-                    'actual_questions' => 0
-                ]);
-
-                // 返回占位题目,让用户知道有数据缺失
-                return [
-                    [
-                        'id' => 'missing_data',
-                        'question_number' => 1,
-                        'question_bank_id' => null,
-                        'question_type' => 'info',
-                        'content' => "⚠️ 数据异常:试卷显示应有 {$paper->question_count} 道题目,但未找到题目数据。这通常是试卷创建过程中断导致的。请联系管理员或重新创建试卷。",
-                        'answer' => '',
-                        'score' => 0,
-                        'is_missing_data' => true
-                    ]
-                ];
-            }
-
-            if ($questions->isEmpty()) {
-                \Log::info('试卷确实没有题目', ['paper_id' => $this->selectedPaperId]);
-                return [
-                    [
-                        'id' => 'no_questions',
-                        'question_number' => 1,
-                        'question_bank_id' => null,
-                        'question_type' => 'info',
-                        'content' => '该试卷暂无题目数据',
-                        'answer' => '',
-                        'score' => 0,
-                        'is_empty' => true
-                    ]
-                ];
-            }
-
-            // 获取题目详情
-            $questionBankService = app(\App\Services\QuestionBankService::class);
-            $questionIds = $questions->pluck('question_bank_id')->filter()->unique()->toArray();
-
-            if (empty($questionIds)) {
-                \Log::info('题目没有关联题库ID', ['paper_id' => $this->selectedPaperId]);
-                // 返回基本的题目信息,不包含题库详情
-                return $questions->map(function($q) {
-                    return [
-                        'id' => $q->id,
-                        'question_number' => $q->question_number,
-                        'question_bank_id' => $q->question_bank_id,
-                        'question_type' => $q->question_type,
-                        'content' => '题目内容未关联到题库',
-                        'answer' => '',
-                        'score' => $q->score ?? 5,
-                    ];
-                })->toArray();
-            }
-
-            $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
-            $questionDetails = collect($questionsResponse['data'] ?? [])->keyBy('id');
-
-            return $questions->map(function($q) use ($questionDetails) {
-                $detail = $questionDetails->get($q->question_bank_id);
-                return [
-                    'id' => $q->id,
-                    'question_number' => $q->question_number,
-                    'question_bank_id' => $q->question_bank_id,
-                    'question_type' => $q->question_type,
-                    'content' => $detail['stem'] ?? '题目内容缺失',
-                    'answer' => $detail['answer'] ?? '',
-                    'score' => $q->score ?? 5,
-                    'kp_code' => $q->knowledge_point, // 从本地数据库获取知识点代码
-                ];
-            })->toArray();
-        } catch (\Exception $e) {
-            \Log::error('获取试卷题目失败', [
-                'paper_id' => $this->selectedPaperId,
-                'error' => $e->getMessage(),
-                'trace' => $e->getTraceAsString()
-            ]);
-            return [
-                [
-                    'id' => 'error',
-                    'question_number' => 1,
-                    'question_bank_id' => null,
-                    'question_type' => 'error',
-                    'content' => '获取题目数据时发生错误:' . $e->getMessage(),
-                    'answer' => '',
-                    'score' => 0,
-                    'is_error' => true
-                ]
-            ];
-        }
+        return app(ExamPaperService::class)->getPaperQuestions($this->selectedPaperId);
     }
 
     public function updatedTeacherId($value): void
@@ -481,18 +95,17 @@ class UploadExamPaper extends Page
         $this->selectedPaperId = null;
         $this->questionGrades = [];
     }
-    
+
     public function updatedStudentId($value): void
     {
         // 当学生选择变化时,清空已选试卷
         $this->selectedPaperId = null;
         $this->questionGrades = [];
     }
-    
+
     public function updatedMode($value): void
     {
         // 切换模式时重置相关字段
-        $this->uploadedImage = null;
         $this->selectedPaperId = null;
         $this->questionGrades = [];
     }
@@ -539,128 +152,6 @@ class UploadExamPaper extends Page
         $this->dispatch('gradingComplete');
     }
 
-    public function submitUpload(): void
-    {
-        if (!$this->teacherId) {
-            Notification::make()
-                ->title('请选择老师')
-                ->danger()
-                ->send();
-            return;
-        }
-
-        if (!$this->studentId) {
-            Notification::make()
-                ->title('请选择学生')
-                ->danger()
-                ->send();
-            return;
-        }
-
-        // 获取表单数据
-        $formData = $this->data;
-
-        if (empty($formData['image'])) {
-            Notification::make()
-                ->title('请上传图片')
-                ->danger()
-                ->send();
-            return;
-        }
-
-        if (empty($formData['paper_name'])) {
-            Notification::make()
-                ->title('请填写试卷名称')
-                ->danger()
-                ->send();
-            return;
-        }
-
-        if (empty($formData['class'])) {
-            Notification::make()
-                ->title('请选择班级')
-                ->danger()
-                ->send();
-            return;
-        }
-
-        if (empty($formData['student_name'])) {
-            Notification::make()
-                ->title('请填写学生姓名')
-                ->danger()
-                ->send();
-            return;
-        }
-
-        $this->isUploading = true;
-
-        try {
-            // 处理图片(可能是单张或多张)
-            $images = $formData['image'];
-            if (!is_array($images)) {
-                $images = [$images];
-            }
-
-            $paths = [];
-            foreach ($images as $image) {
-                if ($image) {
-                    $paths[] = storage_path('app/public/' . $image);
-                }
-            }
-
-            if (empty($paths)) {
-                throw new \Exception('图片保存失败');
-            }
-
-            $paperId = 'paper_' . time() . '_' . substr(md5(uniqid()), 0, 8);
-
-            // AI分析服务调用
-            $response = \Http::timeout(300)
-                ->post('http://localhost:5016/analyze-exam', [
-                    'paper_id' => $paperId,
-                    'paper_name' => $formData['paper_name'],
-                    'student_name' => $formData['student_name'],
-                    'class_name' => $formData['class'],
-                    'paper_type' => $formData['paper_type'],
-                    'subject' => $formData['paper_subject'],
-                    'image_files' => $paths,
-                ]);
-
-            if ($response->successful()) {
-                $result = $response->json();
-                $this->saveAnalysisResult($result, $paperId);
-                $this->analysisResult = $result;
-                Notification::make()
-                    ->title('分析完成')
-                    ->success()
-                    ->send();
-            } else {
-                $this->analysisError = '分析服务响应失败: ' . $response->status();
-                Notification::make()
-                    ->title('分析失败')
-                    ->body($this->analysisError)
-                    ->error()
-                    ->send();
-            }
-
-            // 重置表单
-            $this->teacherId = null;
-            $this->studentId = null;
-            $this->uploadedImage = null;
-            $this->paperType = null;
-
-        } catch (\Exception $e) {
-            Notification::make()
-                ->title('上传失败')
-                ->body($e->getMessage())
-                ->danger()
-                ->send();
-        } finally {
-            $this->isUploading = false;
-            $this->analyzing = false;
-        }
-    }
-
     #[On('teacherChanged')]
     public function updateTeacherId($teacherId)
     {
@@ -674,11 +165,6 @@ class UploadExamPaper extends Page
         $this->studentId = $studentId;
     }
 
-    public function removeImage(): void
-    {
-        $this->uploadedImage = null;
-    }
-    
     /**
      * 提交手动评分
      */
@@ -693,8 +179,11 @@ class UploadExamPaper extends Page
         }
 
         // 将 gradingData 转换为 questionGrades 格式
-        $this->convertGradingDataToQuestionGrades();
-
+        // 注意:这里假设子组件已经传递了处理好的 questionGrades,或者我们在这里再次处理
+        // 如果子组件传递了 questionGrades,我们直接使用它。
+        // 如果没有(比如直接在父组件调用),我们需要 convertGradingDataToQuestionGrades。
+        // 但目前逻辑是子组件调用 handleSubmitFromParent 传递数据。
+        
         if (empty($this->questionGrades)) {
             Notification::make()
                 ->title('请至少为一道题目评分')
@@ -706,10 +195,10 @@ class UploadExamPaper extends Page
         try {
             // 准备数据发送到 LearningAnalytics
             $analyticsData = [];
-            
+
             // 获取题目详情以便查找kp_code
             $questionsMap = collect($this->selectedPaperQuestions)->keyBy('id');
-            
+
             // 收集需要从API补充信息的题目ID
             $missingKpCodeQuestionIds = [];
 
@@ -718,13 +207,13 @@ class UploadExamPaper extends Page
                 if (!$question) {
                     continue;
                 }
-                
+
                 // 优先使用本地存储的 kp_code
                 if (empty($question['kp_code'])) {
                     $missingKpCodeQuestionIds[] = $questionId;
                 }
             }
-            
+
             // 如果有缺失 kp_code 的题目,尝试从 API 获取
             $apiDetailsMap = collect([]);
             if (!empty($missingKpCodeQuestionIds)) {
@@ -732,7 +221,7 @@ class UploadExamPaper extends Page
                     ->map(fn($qId) => $questionsMap->get($qId)['question_bank_id'] ?? null)
                     ->filter()
                     ->toArray();
-                    
+
                 if (!empty($questionBankIds)) {
                     $questionBankService = app(\App\Services\QuestionBankService::class);
                     $questionsDetails = $questionBankService->getQuestionsByIds($questionBankIds);
@@ -745,9 +234,9 @@ class UploadExamPaper extends Page
                 if (!$question) {
                     continue;
                 }
-                
+
                 $kpCode = $question['kp_code'];
-                
+
                 // 如果本地没有,尝试从API结果中获取
                 if (empty($kpCode)) {
                     $detail = $apiDetailsMap->get($question['question_bank_id']);
@@ -864,8 +353,6 @@ class UploadExamPaper extends Page
 
             // 步骤2: 触发 AI 分析(包含掌握度更新和学习报告生成)
             try {
-                $paper = \App\Models\Paper::find($this->selectedPaperId);
-                
                 // 构造 AI 分析请求数据
                 $analysisQuestions = [];
                 foreach ($this->questionGrades as $questionId => $grade) {
@@ -873,13 +360,13 @@ class UploadExamPaper extends Page
                     if (!$question) {
                         continue;
                     }
-                    
+
                     $kpCode = $question['kp_code'];
                     if (empty($kpCode)) {
                         $detail = $apiDetailsMap->get($question['question_bank_id']);
                         $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null;
                     }
-                    
+
                     $analysisQuestions[] = [
                         'question_id' => $question['question_bank_id'],
                         'question_number' => (string)$question['question_number'],
@@ -894,7 +381,7 @@ class UploadExamPaper extends Page
                         'ocr_confidence' => 1.0, // 手动评分置信度为1
                     ];
                 }
-                
+
                 $analysisData = [
                     'exam_id' => $this->selectedPaperId,
                     'student_id' => $this->studentId,
@@ -904,7 +391,7 @@ class UploadExamPaper extends Page
                     'analysis_type' => 'mastery',
                     'questions' => $analysisQuestions,
                 ];
-                
+
                 // 调用统一的 AI 分析接口
                 \Log::info('准备调用submitOCRAnalysis API', [
                     'paper_id' => $this->selectedPaperId,
@@ -923,19 +410,19 @@ class UploadExamPaper extends Page
                     'analysis_result_keys' => is_array($analysisResult) ? array_keys($analysisResult) : 'not_array',
                     'analysis_result' => $analysisResult
                 ]);
-                
+
                 // 保存 analysis_id 到 Paper 表
                 if (isset($analysisResult['analysis_id'])) {
                     \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([
                         'analysis_id' => $analysisResult['analysis_id'],
                     ]);
-                    
+
                     \Log::info('已保存 analysis_id', [
                         'paper_id' => $this->selectedPaperId,
                         'analysis_id' => $analysisResult['analysis_id']
                     ]);
                 }
-                
+
             } catch (\Exception $analysisError) {
                 // AI 分析失败不影响主流程
                 \Log::warning('触发AI分析失败', [
@@ -978,273 +465,6 @@ class UploadExamPaper extends Page
         }
     }
 
-    /**
-     * 将 gradingData 转换为 questionGrades 格式
-     * gradingData: 索引数组 [{is_correct: bool, score: float}]
-     * questionGrades: 题目ID为键的数组 [questionId => {is_correct: bool, score: float, student_answer: string}]
-     */
-    private function convertGradingDataToQuestionGrades(): void
-    {
-        $this->questionGrades = [];
-
-        // 遍历 questions 数组(包含题目信息)
-        foreach ($this->questions as $index => $question) {
-            // 获取对应索引的 gradingData
-            $grading = $this->gradingData[$index] ?? null;
-
-            // 只有当 grading 不为空且有评分数据时才添加
-            if ($grading && (
-                (isset($grading['is_correct']) && $grading['is_correct'] !== null) ||
-                (isset($grading['score']) && $grading['score'] !== null)
-            )) {
-                $questionId = $question['id'];
-
-                // 处理 is_correct 值(字符串 'true'/'false' 或布尔值)
-                $isCorrect = $grading['is_correct'] ?? null;
-                if ($isCorrect === 'true') {
-                    $isCorrect = true;
-                } elseif ($isCorrect === 'false') {
-                    $isCorrect = false;
-                }
-                // 如果 is_correct 为 null,保持为 null(不要转换为布尔值)
-
-                // 处理 score 值
-                $score = $grading['score'] ?? null;
-                if ($score !== null && $score !== '') {
-                    $score = is_numeric($score) ? (float)$score : null;
-                }
-
-                // **关键修复**:根据题型处理缺失的字段
-                if ($question['question_type'] === 'choice') {
-                    // 选择题:只有 is_correct,需要自动计算分数
-                    if ($isCorrect === true) {
-                        $score = $question['score'] ?? 0; // 正确给满分
-                    } elseif ($isCorrect === false) {
-                        $score = 0; // 错误给0分
-                    }
-                } else {
-                    // 填空/解答题:只有 score,需要自动计算 is_correct
-                    if ($score !== null) {
-                        // 动态计算:得分等于满分才算正确
-                        $maxScore = $question['score'] ?? 0;
-                        $isCorrect = ($score >= $maxScore && $maxScore > 0);
-                    }
-                }
-
-                // 获取学生答案(优先使用 gradingData 中的值,如果没有则使用题目中的值)
-                $studentAnswer = $grading['student_answer'] ?? $question['student_answer'] ?? '';
-
-                // 对于选择题,如果学生答案为空,基于评分推断
-                if (empty($studentAnswer) && $question['question_type'] === 'choice') {
-                    if ($isCorrect === true) {
-                        // 如果选"正确",学生答案就是正确答案
-                        $studentAnswer = $question['correct_answer'] ?? '正确答案';
-                    } elseif ($isCorrect === false) {
-                        // 如果选"错误",学生答案可以为空或者设置为特殊标记
-                        $studentAnswer = '错误答案';
-                    }
-                }
-
-                // 转换格式
-                $this->questionGrades[$questionId] = [
-                    'is_correct' => $isCorrect,
-                    'score' => $score,
-                    'student_answer' => $studentAnswer,
-                ];
-            }
-        }
-
-        \Log::info('转换评分数据', [
-            'grading_data_count' => count(array_filter($this->gradingData ?? [])),
-            'question_grades_count' => count($this->questionGrades),
-            'questions_count' => count($this->questions ?? []),
-            'sample_question_grades' => array_slice($this->questionGrades, 0, 2, true),
-        ]);
-    }
-
-    #[Computed]
-    public function gradingProgress(): string
-    {
-        $gradedCount = count(array_filter($this->gradingData ?? []));
-        $totalCount = count($this->questions ?? []);
-        return "已评分:{$gradedCount}/{$totalCount}题";
-    }
-
-    public function startAnalysis(): void
-    {
-        $this->analyzing = true;
-        $this->analysisError = null;
-
-        try {
-            $this->submitUpload();
-        } catch (\Exception $e) {
-            $this->analysisError = $e->getMessage();
-            $this->analyzing = false;
-        }
-    }
-
-    public function saveGrading(): void
-    {
-        $this->submitManualGrading();
-    }
-
-    public function updatedSelectedPaperId($value): void
-    {
-        if (empty($value)) {
-            $this->questions = [];
-            $this->gradingData = [];
-            $this->showGrading = false;
-            return;
-        }
-
-        // 加载试卷信息和题目
-        $this->loadPaperForGrading($value);
-    }
-
-    public function loadPaperForGrading($paperId): void
-    {
-        try {
-            $paper = \App\Models\Paper::where('paper_id', $paperId)->first();
-            if (!$paper) {
-                Notification::make()
-                    ->title('试卷不存在')
-                    ->danger()
-                    ->send();
-                return;
-            }
-
-            // 设置试卷信息
-            $this->paperName = $paper->paper_name;
-            $this->paperClass = $paper->difficulty_category ?? '未设置';
-            $this->paperStudent = $paper->student_id;
-            $this->paperDate = $paper->created_at->format('Y-m-d H:i');
-
-            // 加载题目
-            $paperWithQuestions = \App\Models\Paper::with(['questions' => function($query) {
-                $query->orderBy('question_number');
-            }])->where('paper_id', $paperId)->first();
-
-            $questions = $paperWithQuestions ? $paperWithQuestions->questions : collect([]);
-
-            // 如果没有正确答案,先尝试从题库API获取
-            $apiDetailsMap = new \Illuminate\Support\Collection();
-            if (!$questions->isEmpty()) {
-                $questionBankIds = $questions->where('question_bank_id', '!=', null)->pluck('question_bank_id')->unique()->toArray();
-                if (!empty($questionBankIds)) {
-                    try {
-                        $questionBankService = app(\App\Services\QuestionBankService::class);
-                        $apiResponse = $questionBankService->getQuestionsByIds($questionBankIds);
-
-                        if (!empty($apiResponse['data'])) {
-                            foreach ($apiResponse['data'] as $detail) {
-                                $apiDetailsMap->put($detail['id'], $detail);
-                            }
-                            \Log::info('成功从题库API获取题目详情', [
-                                'count' => count($apiResponse['data']),
-                                'ids' => array_keys($apiResponse['data'])
-                            ]);
-                        }
-                    } catch (\Exception $e) {
-                        \Log::warning('获取题库详情失败', ['error' => $e->getMessage()]);
-                    }
-                }
-            }
-
-            if ($questions->isEmpty()) {
-                $this->questions = [
-                    [
-                        'id' => 'no_questions',
-                        'question_number' => 1,
-                        'question_type' => 'info',
-                        'content' => '该试卷暂无题目数据',
-                        'answer' => '',
-                        'score' => 0,
-                        'is_empty' => true
-                    ]
-                ];
-            } else {
-                $this->questions = $questions->map(function($question, $index) use ($apiDetailsMap) {
-                    // 从 API 获取正确答案(优先使用 API 数据)
-                    $correctAnswer = $question->correct_answer;
-                    if (empty($correctAnswer) && $question->question_bank_id && $apiDetailsMap->has($question->question_bank_id)) {
-                        $detail = $apiDetailsMap->get($question->question_bank_id);
-                        $correctAnswer = $detail['answer'] ?? $detail['correct_answer'] ?? '';
-                    }
-
-                    return [
-                        'id' => $question->id,
-                        'question_number' => $question->question_number,
-                        'question_type' => $question->question_type,
-                        'question_text' => $question->question_text,
-                        'content' => $question->question_text,
-                        'options' => json_decode($question->options, true) ?: [],
-                        'answer' => $correctAnswer,
-                        'correct_answer' => $correctAnswer,
-                        'student_answer' => '', // 学生答案暂不显示,等后续完善
-                        'score' => $question->score,
-                        'max_score' => $question->score,
-                        'question_bank_id' => $question->question_bank_id,
-                        'is_empty' => false
-                    ];
-                })->toArray();
-            }
-
-            // 初始化评分数据
-            $this->gradingData = array_fill(0, count($this->questions), ['score' => null, 'is_correct' => null, 'comment' => '']);
-            $this->showGrading = true;
-
-        } catch (\Exception $e) {
-            \Log::error('加载试卷题目失败', [
-                'paper_id' => $paperId,
-                'error' => $e->getMessage()
-            ]);
-
-            Notification::make()
-                ->title('加载失败')
-                ->body($e->getMessage())
-                ->danger()
-                ->send();
-        }
-    }
-
-    private function saveAnalysisResult(array $result, string $paperId): void
-    {
-        try {
-            \DB::beginTransaction();
-
-            // 保存试卷基本信息
-            $examPaper = \App\Models\Paper::create([
-                'paper_id' => $paperId,
-                'paper_name' => $result['paper_name'] ?? '未命名试卷',
-                'student_id' => $this->studentId,
-                'teacher_id' => $this->teacherId,
-                'paper_type' => $result['paper_type'] ?? 'quiz',
-                'question_count' => count($result['questions'] ?? []),
-                'total_score' => $result['total_score'] ?? 0,
-                'status' => 'completed',
-            ]);
-
-            // 保存题目信息
-            foreach ($result['questions'] ?? [] as $index => $questionData) {
-                \App\Models\PaperQuestion::create([
-                    'paper_id' => $paperId,
-                    'question_number' => $index + 1,
-                    'question_text' => $questionData['question_text'] ?? '',
-                    'question_type' => $questionData['question_type'] ?? 'choice',
-                    'options' => json_encode($questionData['options'] ?? []),
-                    'correct_answer' => $questionData['correct_answer'] ?? '',
-                    'score' => $questionData['score'] ?? 1,
-                ]);
-            }
-
-            \DB::commit();
-
-        } catch (\Exception $e) {
-            \DB::rollBack();
-            \Log::error('保存分析结果失败: ' . $e->getMessage());
-        }
-    }
-
     /**
      * 查看记录详情 - 使用页面跳转
      */

+ 77 - 34
app/Livewire/UploadExam/GradingPanel.php

@@ -3,8 +3,9 @@
 namespace App\Livewire\UploadExam;
 
 use Livewire\Component;
-use App\Services\LearningAnalyticsService;
+use App\Services\ExamPaperService;
 use Filament\Notifications\Notification;
+use Livewire\Attributes\On;
 
 class GradingPanel extends Component
 {
@@ -13,10 +14,12 @@ class GradingPanel extends Component
     public ?string $selectedPaperId = null;
     public array $questions = [];
     public array $gradingData = [];
+    public array $questionGrades = []; // 添加这个属性
     public ?string $paperName = null;
     public ?string $paperClass = null;
     public ?string $paperStudent = null;
     public ?string $paperDate = null;
+    public bool $showGrading = false; // 添加这个属性
 
     protected $listeners = [
         'loadPaper' => 'loadPaper',
@@ -34,6 +37,11 @@ class GradingPanel extends Component
         $this->studentId = $studentId;
         $this->selectedPaperId = $selectedPaperId;
         $this->questions = $questions;
+        
+        // 如果传入了题目,初始化评分数据
+        if (!empty($questions)) {
+            $this->initializeGradingData();
+        }
     }
 
     #[On('loadPaper')]
@@ -49,6 +57,11 @@ class GradingPanel extends Component
         if (!empty($value)) {
             // 清空评分数据
             $this->gradingData = [];
+            $this->loadPaperForGrading($value);
+        } else {
+            $this->questions = [];
+            $this->gradingData = [];
+            $this->showGrading = false;
         }
     }
 
@@ -142,55 +155,85 @@ class GradingPanel extends Component
 
     public function render()
     {
-        // 直接复用父页面的逻辑获取题目数据
-        if (!empty($this->selectedPaperId)) {
-            $this->questions = $this->loadPaperQuestionsDirectly();
-        }
-
         return view('livewire.upload-exam.grading-panel');
     }
 
-    // 直接加载题目数据(复用父页面逻辑)
-    private function loadPaperQuestionsDirectly(): array
+    public function loadPaperForGrading($paperId): void
     {
         try {
-            \Log::info('GradingPanel: 开始加载题目', ['paper_id' => $this->selectedPaperId]);
-
-            $paper = \App\Models\Paper::where('paper_id', $this->selectedPaperId)->first();
+            $paper = \App\Models\Paper::where('paper_id', $paperId)->first();
             if (!$paper) {
-                \Log::warning('GradingPanel: 试卷不存在', ['paper_id' => $this->selectedPaperId]);
-                return [];
+                Notification::make()
+                    ->title('试卷不存在')
+                    ->danger()
+                    ->send();
+                return;
             }
 
-            $questions = $paper->questions()->orderBy('question_number')->get();
-            \Log::info('GradingPanel: 查询到题目', ['count' => $questions->count()]);
+            // 设置试卷信息
+            $this->paperName = $paper->paper_name;
+            $this->paperClass = $paper->difficulty_category ?? '未设置';
+            $this->paperStudent = $paper->student_id;
+            $this->paperDate = $paper->created_at->format('Y-m-d H:i');
+
+            // 使用 Service 加载题目
+            $questions = app(ExamPaperService::class)->getPaperQuestions($paperId);
+            
+            // 转换 Service 返回的题目格式以适配 GradingPanel 的视图
+            // Service 返回的格式:
+            // [
+            //     'id' => $q->id,
+            //     'question_number' => $q->question_number,
+            //     'question_bank_id' => $q->question_bank_id,
+            //     'question_type' => $q->question_type,
+            //     'content' => $detail['stem'] ?? '题目内容缺失',
+            //     'answer' => $detail['answer'] ?? '',
+            //     'score' => $q->score ?? 5,
+            //     'kp_code' => $q->knowledge_point,
+            // ]
+            
+            $this->questions = collect($questions)->map(function($q) {
+                // 如果是特殊信息行(如 no_questions, missing_data),直接返回
+                if (isset($q['is_empty']) || isset($q['is_missing_data']) || isset($q['is_error'])) {
+                    return $q;
+                }
 
-            if ($questions->isEmpty()) {
-                \Log::warning('GradingPanel: 题目为空');
-                // 直接返回空数组,不要返回提示信息
-                return [];
-            }
-
-            // 无论如何都返回题目数据,即使API失败
-            $processedQuestions = $questions->map(function($q) {
                 return [
-                    'id' => $q->id,
-                    'question_number' => $q->question_number,
-                    'question_type' => $q->question_type,
-                    'content' => $q->question_text,
-                    'answer' => '', // 可以为空
-                    'score' => $q->score ?? 5,
-                    'question_bank_id' => $q->question_bank_id,
+                    'id' => $q['id'],
+                    'question_number' => $q['question_number'],
+                    'question_type' => $q['question_type'],
+                    'question_text' => $q['content'], // 视图可能使用 question_text
+                    'content' => $q['content'],
+                    'options' => [], // 暂时留空,如果需要选项显示需要从 Service 获取更多详情
+                    'answer' => $q['answer'],
+                    'correct_answer' => $q['answer'],
+                    'student_answer' => '',
+                    'score' => $q['score'],
+                    'max_score' => $q['score'],
+                    'question_bank_id' => $q['question_bank_id'],
                     'is_empty' => false
                 ];
             })->toArray();
 
-            \Log::info('GradingPanel: 处理后的题目', ['count' => count($processedQuestions)]);
+            $this->initializeGradingData();
+            $this->showGrading = true;
 
-            return $processedQuestions;
         } catch (\Exception $e) {
-            \Log::error('GradingPanel: 加载失败', ['error' => $e->getMessage()]);
-            return [];
+            \Log::error('加载试卷题目失败', [
+                'paper_id' => $paperId,
+                'error' => $e->getMessage()
+            ]);
+
+            Notification::make()
+                ->title('加载失败')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
         }
     }
+    
+    private function initializeGradingData()
+    {
+        $this->gradingData = array_fill(0, count($this->questions), ['score' => null, 'is_correct' => null, 'comment' => '']);
+    }
 }

+ 8 - 53
app/Livewire/UploadExam/UploadForm.php

@@ -17,12 +17,6 @@ class UploadForm extends Component
     public ?string $studentId = null;
     public $uploadedImages = [];
     public bool $isUploading = false;
-    public ?int $currentOcrRecordId = null;
-    public ?string $ocrStatus = null;
-
-    protected $listeners = [
-        'uploadComplete' => 'handleUploadComplete',
-    ];
 
     public function mount($teacherId = null, $studentId = null)
     {
@@ -65,17 +59,17 @@ class UploadForm extends Component
                 'status' => 'processing',
             ]);
 
-            $this->currentOcrRecordId = $ocrRecord->id;
-            $this->ocrStatus = 'processing';
-
             // 派发 OCR 处理任务
             ProcessOCRRecord::dispatch($ocrRecord->id);
 
-            $this->dispatch('uploadComplete', [
-                'ocrRecordId' => $ocrRecord->id,
-                'success' => true,
-                'message' => '图片上传成功,正在进行 OCR 识别...',
-            ]);
+            Notification::make()
+                ->title('上传成功')
+                ->body('图片已上传,正在进行 OCR 识别...')
+                ->success()
+                ->send();
+
+            // 立即跳转到 OCR 详情页,不等待识别完成
+            $this->redirect('/admin/ocr-record-view/' . $ocrRecord->id);
 
         } catch (\Exception $e) {
             Notification::make()
@@ -83,11 +77,6 @@ class UploadForm extends Component
                 ->body($e->getMessage())
                 ->danger()
                 ->send();
-
-            $this->dispatch('uploadComplete', [
-                'success' => false,
-                'message' => $e->getMessage(),
-            ]);
         } finally {
             $this->isUploading = false;
         }
@@ -106,40 +95,6 @@ class UploadForm extends Component
         $this->uploadedImages = array_values($this->uploadedImages);
     }
 
-    public function handleUploadComplete($data)
-    {
-        if ($data['success'] ?? false) {
-            // 清空表单
-            $this->resetForm();
-        }
-    }
-
-    public function checkOcrStatus()
-    {
-        if (!$this->currentOcrRecordId) {
-            return;
-        }
-
-        $ocrRecord = \App\Models\OCRRecord::find($this->currentOcrRecordId);
-
-        if ($ocrRecord) {
-            $this->ocrStatus = $ocrRecord->status;
-
-            if ($ocrRecord->status === 'completed') {
-                // OCR完成,跳转到详情页
-                $url = '/admin/exam-analysis?recordId=' . $ocrRecord->id . '&studentId=' . $this->studentId;
-                $this->redirect($url);
-            } elseif ($ocrRecord->status === 'failed') {
-                Notification::make()
-                    ->title('OCR识别失败')
-                    ->body($ocrRecord->error_message ?? '未知错误')
-                    ->danger()
-                    ->send();
-                $this->currentOcrRecordId = null;
-            }
-        }
-    }
-
     public function render()
     {
         return view('livewire.upload-exam.upload-form');

+ 6 - 5
app/Services/OCRService.php

@@ -263,10 +263,11 @@ class OCRService
             'questions_processed' => $processedCount
         ]);
 
-        // 发送分析请求到 LearningAnalytics
-        if ($processedCount > 0) {
-            $this->submitToAnalysis($ocrRecord, $questions);
-        }
+        // 不再自动提交分析,让用户在 OCR 详情页先生成题库题目
+        // 用户需要在 ocr-record-view 页面手动点击"生成题库题目"和"提交分析"
+        // if ($processedCount > 0) {
+        //     $this->submitToAnalysis($ocrRecord, $questions);
+        // }
     }
 
     /**
@@ -276,7 +277,7 @@ class OCRService
     {
         try {
             $analysisData = [
-                'exam_id' => $ocrRecord->exam_id,
+                'exam_id' => $ocrRecord->exam_id ?? ('ocr_' . $ocrRecord->id), // 使用 OCR 记录 ID 作为后备
                 'student_id' => $ocrRecord->student_id,
                 'ocr_record_id' => $ocrRecord->id,
                 'teacher_name' => 'System', // 或者是上传者的名字

+ 3 - 2
app/Services/QuestionBankService.php

@@ -936,9 +936,10 @@ class QuestionBankService
                 ];
             }
 
-            // 直接调用QuestionBank API的异步端点,提供回调URL
+            // 直接调用QuestionBank API的异步端点,提供回调URL
+            // 注意: baseUrl 已经包含 /api,所以这里只需要 /ocr/questions/generate-from-ocr
             $response = Http::timeout(60)
-                ->post($this->baseUrl . '/api/questions/generate-from-ocr', [
+                ->post($this->baseUrl . '/ocr/questions/generate-from-ocr', [
                     'ocr_record_id' => $ocrRecordId,
                     'questions' => $formattedQuestions,
                     'grade_level' => $gradeLevel,

+ 1 - 63
resources/views/livewire/upload-exam/upload-form.blade.php

@@ -1,4 +1,4 @@
-<div wire:poll.2000ms="checkOcrStatus">
+<div>
     <div class="bg-white shadow-md rounded-lg p-6">
         <h2 class="text-xl font-semibold text-gray-900 mb-4">上传试卷照片</h2>
 
@@ -51,66 +51,4 @@
             </div>
         </form>
     </div>
-
-    {{-- OCR状态显示 --}}
-    @if($currentOcrRecordId)
-        <div class="mt-4
-            @if($ocrStatus === 'processing')
-                bg-blue-50 border-blue-200
-            @elseif($ocrStatus === 'completed')
-                bg-green-50 border-green-200
-            @elseif($ocrStatus === 'failed')
-                bg-red-50 border-red-200
-            @else
-                bg-gray-50 border-gray-200
-            @endif
-            border rounded-md p-4">
-            <div class="flex items-center">
-                @if($ocrStatus === 'processing')
-                    <svg class="animate-spin h-5 w-5 text-blue-600 mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-                    </svg>
-                @elseif($ocrStatus === 'completed')
-                    <svg class="h-5 w-5 text-green-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
-                    </svg>
-                @elseif($ocrStatus === 'failed')
-                    <svg class="h-5 w-5 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
-                    </svg>
-                @endif
-                <div class="flex-1">
-                    @if($ocrStatus === 'processing')
-                        <p class="text-sm font-medium text-blue-800">正在处理OCR识别...</p>
-                        <p class="text-xs text-blue-600 mt-1">请稍候,这可能需要几分钟时间</p>
-                    @elseif($ocrStatus === 'completed')
-                        <p class="text-sm font-medium text-green-800">OCR识别完成!正在跳转到详情页...</p>
-                    @elseif($ocrStatus === 'failed')
-                        <p class="text-sm font-medium text-red-800">OCR识别失败</p>
-                        <p class="text-xs text-red-600 mt-1">请检查图片质量或稍后重试</p>
-                    @else
-                        <p class="text-sm font-medium text-gray-800">等待处理...</p>
-                    @endif
-                </div>
-                @if($ocrStatus === 'completed')
-                    <button type="button" wire:click="checkOcrStatus" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700">
-                        查看详情
-                    </button>
-                @endif
-            </div>
-        </div>
-    @endif
 </div>
-
-<script>
-    document.addEventListener('livewire:initialized', () => {
-        // 处理上传完成事件
-        Livewire.on('uploadComplete', (event) => {
-            if (event.success) {
-                // 清空表单
-                @this.resetForm();
-            }
-        });
-    });
-</script>