Ver Fonte

逐步修复 bug——处理删除题库题目不刷新的问题

yemeishu há 1 mês atrás
pai
commit
22fea0a8a7

+ 60 - 81
app/Filament/Pages/StudentDashboard.php

@@ -2,6 +2,8 @@
 
 namespace App\Filament\Pages;
 
+use App\Models\Student;
+use App\Models\Teacher;
 use App\Services\LearningAnalyticsService;
 use BackedEnum;
 use Filament\Pages\Page;
@@ -13,6 +15,7 @@ use UnitEnum;
 use Livewire\Attributes\Layout;
 use Livewire\Attributes\Title;
 use Livewire\Attributes\On;
+use Livewire\Attributes\Computed;
 
 class StudentDashboard extends Page
 {
@@ -35,73 +38,45 @@ class StudentDashboard extends Page
     public array $dashboardData = [];
     public bool $isLoading = false;
     public string $errorMessage = '';
-    public array $teachers = [];
-    public array $students = [];
+    // teachers 和 students 现在是 Computed 属性,不再需要声明
 
     public function mount(Request $request): void
     {
-        // 加载老师列表
-        $this->loadTeachers();
-
-        // 从请求中获取老师ID或使用默认值
-        $this->teacherId = $request->input('teacher_id', $this->getDefaultTeacherId());
-
-        // 根据老师ID加载学生列表
-        $this->loadStudentsByTeacher();
-
-        // 从请求中获取学生ID或使用默认值
-        $this->studentId = $request->input('student_id', $this->getDefaultStudentId());
-    }
-
-    /**
-     * 获取默认老师ID(列表中的第一个老师)
-     */
-    private function getDefaultTeacherId(): string
-    {
-        return !empty($this->teachers) ? $this->teachers[0]->teacher_id : '';
-    }
-
-    /**
-     * 获取默认学生ID(列表中的第一个学生)
-     */
-    private function getDefaultStudentId(): string
-    {
-        return !empty($this->students) ? $this->students[0]->student_id : '';
+        // 从请求中获取老师ID
+        $this->teacherId = $request->input('teacher_id', '');
+        // 从请求中获取学生ID
+        $this->studentId = $request->input('student_id', '');
     }
 
-    /**
-     * 从MySQL加载老师列表
-     */
-    public function loadTeachers(): void
+    #[Computed]
+    public function teachers(): array
     {
         try {
-            // 首先获取teachers表中的老师
-            $this->teachers = DB::connection('remote_mysql')
-                ->table('teachers as t')
-                ->leftJoin('users as u', 't.teacher_id', '=', 'u.user_id')
+            $teachers = Teacher::query()
+                ->leftJoin('users as u', 'teachers.teacher_id', '=', 'u.user_id')
                 ->select(
-                    't.teacher_id',
-                    't.name',
-                    't.subject',
+                    'teachers.teacher_id',
+                    'teachers.name',
+                    'teachers.subject',
                     'u.username',
                     'u.email'
                 )
-                ->orderBy('t.name')
-                ->get()
-                ->toArray();
+                ->orderBy('teachers.name')
+                ->get();
 
-            // 如果有学生但没有对应的老师记录,添加一个"未知老师"条目
-            $teacherIds = array_column($this->teachers, 'teacher_id');
-            $missingTeacherIds = DB::connection('remote_mysql')
-                ->table('students as s')
+            // 检查是否有学生没有对应的老师记录
+            $teacherIds = $teachers->pluck('teacher_id')->toArray();
+            $missingTeacherIds = Student::query()
                 ->distinct()
-                ->whereNotIn('s.teacher_id', $teacherIds)
+                ->whereNotIn('teacher_id', $teacherIds)
                 ->pluck('teacher_id')
                 ->toArray();
 
+            $teachersArray = $teachers->all();
+
             if (!empty($missingTeacherIds)) {
                 foreach ($missingTeacherIds as $missingId) {
-                    $this->teachers[] = (object) [
+                    $teachersArray[] = (object) [
                         'teacher_id' => $missingId,
                         'name' => '未知老师 (' . $missingId . ')',
                         'subject' => '未知',
@@ -110,51 +85,50 @@ class StudentDashboard extends Page
                     ];
                 }
 
-                // 重新排序
-                usort($this->teachers, function($a, $b) {
+                usort($teachersArray, function($a, $b) {
                     return strcmp($a->name, $b->name);
                 });
             }
+
+            return $teachersArray;
         } catch (\Exception $e) {
             Log::error('加载老师列表失败', [
                 'error' => $e->getMessage()
             ]);
-            $this->teachers = [];
+            return [];
         }
     }
 
-    /**
-     * 根据老师ID加载学生列表
-     */
-    public function loadStudentsByTeacher(): void
+    #[Computed]
+    public function students(): array
     {
-        try {
-            if (empty($this->teacherId)) {
-                $this->students = [];
-                return;
-            }
+        if (empty($this->teacherId)) {
+            return [];
+        }
 
-            $this->students = DB::connection('remote_mysql')
-                ->table('students as s')
-                ->leftJoin('users as u', 's.student_id', '=', 'u.user_id')
-                ->where('s.teacher_id', $this->teacherId)
+        try {
+            return Student::query()
+                ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id')
+                ->where('students.teacher_id', $this->teacherId)
                 ->select(
-                    's.student_id',
-                    's.name',
-                    's.grade',
-                    's.class_name',
+                    'students.student_id',
+                    'students.name',
+                    'students.grade',
+                    'students.class_name',
                     'u.username',
                     'u.email'
                 )
-                ->orderBy('s.name')
+                ->orderBy('students.grade')
+                ->orderBy('students.class_name')
+                ->orderBy('students.name')
                 ->get()
-                ->toArray();
+                ->all();
         } catch (\Exception $e) {
             Log::error('加载学生列表失败', [
                 'teacher_id' => $this->teacherId,
                 'error' => $e->getMessage()
             ]);
-            $this->students = [];
+            return [];
         }
     }
 
@@ -163,11 +137,15 @@ class StudentDashboard extends Page
      */
     public function updatedTeacherId(): void
     {
-        $this->loadStudentsByTeacher();
         // 清空之前选中的学生ID
         $this->studentId = '';
-        // 自动加载第一个学生的数据
-        $this->studentId = $this->getDefaultStudentId();
+    }
+
+    /**
+     * 学生改变时重新加载数据
+     */
+    public function updatedStudentId(): void
+    {
         if (!empty($this->studentId)) {
             $this->loadDashboardData();
         }
@@ -175,6 +153,13 @@ class StudentDashboard extends Page
 
     public function loadDashboardData(): void
     {
+        // 检查是否选择了学生
+        if (empty($this->studentId)) {
+            $this->errorMessage = '请先选择学生';
+            $this->isLoading = false;
+            return;
+        }
+
         $this->isLoading = true;
         $this->errorMessage = '';
 
@@ -245,12 +230,6 @@ class StudentDashboard extends Page
         }
     }
 
-    public function updatedStudentId(): void
-    {
-        // 学生ID更新后自动刷新数据
-        $this->loadDashboardData();
-    }
-
     public function recalculateMastery(string $kpCode): void
     {
         try {

+ 3 - 4
app/Filament/Pages/UploadExamPaper.php

@@ -440,13 +440,12 @@ class UploadExamPaper extends Page
 
             // 创建OCR记录
             $record = OCRRecord::create([
-                'student_id' => $this->studentId,
-                'image_path' => $path,
-                'image_filename' => $filename,
+                'user_id' => $this->studentId,
+                'file_path' => $path,
+                'paper_title' => $filename,
                 'paper_type' => $this->paperType,
                 'status' => 'pending',
                 'total_questions' => 0,
-                'processed_questions' => 0,
             ]);
 
             // 立即更新状态为处理中,提供更好的用户体验

+ 1 - 1
app/Filament/Resources/OCRRecordResource.php

@@ -64,7 +64,7 @@ class OCRRecordResource extends Resource
                 ->searchable()
                 ->required(),
 
-            FileUpload::make('image_path')
+            FileUpload::make('file_path')
                 ->label('卷子图片')
                 ->image()
                 ->directory('ocr-uploads')

+ 2 - 2
app/Filament/Resources/OCRRecordResource/Pages/CreateOCRRecord.php

@@ -21,8 +21,8 @@ class CreateOCRRecord extends CreateRecord
 
     protected function mutateFormDataBeforeCreate(array $data): array
     {
-        if (isset($data['image_path'])) {
-            $data['image_filename'] = basename($data['image_path']);
+        if (isset($data['file_path'])) {
+            // 文件路径会自动处理,不需要额外设置 paper_title
         }
 
         return $data;

+ 11 - 2
app/Models/OCRRecord.php

@@ -24,6 +24,7 @@ class OCRRecord extends Model
         'created_at',
         'updated_at',
         'analysis_id',
+        'image_path', // 添加兼容性字段访问器
     ];
 
     protected $casts = [
@@ -66,10 +67,18 @@ class OCRRecord extends Model
         $this->attributes['user_id'] = $value;
     }
 
+    public function getImagePathAttribute(): string
+    {
+        // 将 file_path 映射为 image_path 以保持兼容性
+        return $this->file_path ?? '';
+    }
+
     public function getImageUrlAttribute(): string
     {
-        if ($this->image_path && file_exists(public_path($this->image_path))) {
-            return asset($this->image_path);
+        // 使用 file_path(通过上面的访问器也可以通过 image_path 访问)
+        $path = $this->file_path;
+        if ($path && file_exists(public_path($path))) {
+            return asset($path);
         }
         return '';
     }

+ 13 - 8
app/Services/OCRService.php

@@ -47,14 +47,9 @@ class OCRService
 
         // 创建OCR记录
         $ocrRecord = OCRRecord::create([
-            'id' => $recordId,
-            'exam_id' => $examId,
-            'student_id' => $studentId,
-            'image_path' => $imagePath,
-            'image_filename' => $image->getClientOriginalName(),
-            'image_size' => $imageSize,
-            'image_width' => $imageWidth,
-            'image_height' => $imageHeight,
+            'user_id' => $studentId,
+            'file_path' => $imagePath,
+            'paper_title' => $image->getClientOriginalName(),
             'status' => 'pending',
         ]);
 
@@ -92,9 +87,19 @@ class OCRService
     protected function dispatchToOcrService(OCRRecord $ocrRecord): void
     {
         try {
+            // 检查图片路径是否存在
+            if (empty($ocrRecord->image_path)) {
+                throw new \Exception('OCR记录缺少图片路径,record_id: ' . $ocrRecord->id);
+            }
+
             // 读取图片文件
             $imagePath = Storage::disk($this->getDisk())->path($ocrRecord->image_path);
 
+            // 确保返回的是字符串路径
+            if (empty($imagePath)) {
+                throw new \Exception('无法获取图片路径: ' . $ocrRecord->image_path);
+            }
+
             if (!file_exists($imagePath)) {
                 throw new \Exception('图片文件不存在: ' . $imagePath);
             }

+ 1 - 1
config/app.php

@@ -65,7 +65,7 @@ return [
     |
     */
 
-    'timezone' => 'UTC',
+    'timezone' => 'Asia/Shanghai',
 
     /*
     |--------------------------------------------------------------------------

+ 30 - 0
database/migrations/2025_11_26_150232_add_student_answer_to_ocr_question_results.php

@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('ocr_question_results', function (Blueprint $table) {
+            // 添加学生答案字段(TEXT类型,不创建索引)
+            $table->text('student_answer')->nullable()->after('question_text')->comment('学生答案');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('ocr_question_results', function (Blueprint $table) {
+            // 删除字段
+            $table->dropColumn('student_answer');
+        });
+    }
+};

+ 40 - 0
database/migrations/2025_11_26_150551_add_score_fields_to_ocr_question_results.php

@@ -0,0 +1,40 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('ocr_question_results', function (Blueprint $table) {
+            // 添加评分相关字段
+            $table->decimal('score_value', 5, 2)->nullable()->after('student_answer')->comment('得分值');
+            $table->boolean('mark_detected')->nullable()->after('score_value')->comment('是否检测到评分标记');
+            $table->decimal('score_confidence', 5, 4)->nullable()->after('mark_detected')->comment('评分置信度');
+
+            // 创建索引
+            $table->index(['score_value', 'mark_detected'], 'idx_ocr_question_results_score');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('ocr_question_results', function (Blueprint $table) {
+            // 删除索引
+            $table->dropIndex('idx_ocr_question_results_score');
+
+            // 删除字段
+            $table->dropColumn('score_value');
+            $table->dropColumn('mark_detected');
+            $table->dropColumn('score_confidence');
+        });
+    }
+};

+ 42 - 0
database/migrations/2025_11_26_150900_add_generation_fields_to_ocr_question_results.php

@@ -0,0 +1,42 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('ocr_question_results', function (Blueprint $table) {
+            // 添加题目生成相关字段
+            $table->string('generation_status', 50)->nullable()->after('score_confidence')->comment('生成状态:pending, generating, completed, failed');
+            $table->string('generation_task_id', 100)->nullable()->after('generation_status')->comment('生成任务ID');
+            $table->text('generation_error')->nullable()->after('generation_task_id')->comment('生成错误信息');
+
+            // 创建索引
+            $table->index('generation_status', 'idx_ocr_question_results_gen_status');
+            $table->index('generation_task_id', 'idx_ocr_question_results_gen_task_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('ocr_question_results', function (Blueprint $table) {
+            // 删除索引
+            $table->dropIndex('idx_ocr_question_results_gen_status');
+            $table->dropIndex('idx_ocr_question_results_gen_task_id');
+
+            // 删除字段
+            $table->dropColumn('generation_status');
+            $table->dropColumn('generation_task_id');
+            $table->dropColumn('generation_error');
+        });
+    }
+};

+ 38 - 0
database/migrations/2025_11_26_151144_add_processed_at_to_ocr_records.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('ocr_records', function (Blueprint $table) {
+            // 添加处理完成时间字段
+            $table->timestamp('processed_at')->nullable()->after('total_questions')->comment('OCR处理完成时间');
+
+            // 创建索引
+            $table->index('processed_at', 'idx_ocr_records_processed_at');
+            $table->index(['status', 'processed_at'], 'idx_ocr_records_status_processed_at');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('ocr_records', function (Blueprint $table) {
+            // 删除索引
+            $table->dropIndex('idx_ocr_records_status_processed_at');
+            $table->dropIndex('idx_ocr_records_processed_at');
+
+            // 删除字段
+            $table->dropColumn('processed_at');
+        });
+    }
+};

+ 59 - 28
resources/views/filament/pages/student-dashboard.blade.php

@@ -20,43 +20,74 @@
             <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
                 <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
                     <div>
-                        <label for="teacher" class="block text-sm font-medium text-gray-700 mb-1">选择老师</label>
-                        <select
-                            id="teacher"
-                            wire:model.live="teacherId"
-                            class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
-                        >
-                            <option value="">请选择老师...</option>
-                            @foreach($this->teachers as $teacher)
-                                <option value="{{ $teacher->teacher_id }}">
-                                    {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
-                                </option>
-                            @endforeach
-                        </select>
+                        <label for="teacher" class="block text-sm font-medium text-gray-700 mb-2">选择老师</label>
+                        <div class="dropdown w-full">
+                            <label tabindex="0" class="btn btn-bordered w-full justify-between">
+                                @if(!empty($teacherId))
+                                    @php
+                                        $selectedTeacher = collect($this->teachers)->firstWhere('teacher_id', $teacherId);
+                                    @endphp
+                                    {{ $selectedTeacher ? trim($selectedTeacher->name ?? $selectedTeacher->teacher_id) . ($selectedTeacher->subject ? " ({$selectedTeacher->subject})" : '') : '请选择老师...' }}
+                                @else
+                                    请选择老师...
+                                @endif
+                                <svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
+                                </svg>
+                            </label>
+                            <ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full max-h-64 overflow-y-auto">
+                                <li>
+                                    <a wire:click="$set('teacherId', '')" class="{{ empty($teacherId) ? 'active' : '' }}">
+                                        请选择老师...
+                                    </a>
+                                </li>
+                                @foreach($this->teachers as $teacher)
+                                    <li>
+                                        <a wire:click="$set('teacherId', '{{ $teacher->teacher_id }}')" class="{{ $teacherId === $teacher->teacher_id ? 'active' : '' }}">
+                                            {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
+                                        </a>
+                                    </li>
+                                @endforeach
+                            </ul>
+                        </div>
                         <p class="mt-1 text-xs text-gray-500">选择要查看的老师</p>
                     </div>
 
                     <div>
-                        <label for="student" class="block text-sm font-medium text-gray-700 mb-1">选择学生</label>
-                        <select
-                            id="student"
-                            wire:model.live="studentId"
-                            class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
-                            @if(empty($teacherId)) disabled @endif
-                        >
-                            <option value="">
+                        <label for="student" class="block text-sm font-medium text-gray-700 mb-2">选择学生</label>
+                        <div class="dropdown w-full">
+                            <label tabindex="0" class="btn btn-bordered w-full justify-between {{ empty($teacherId) ? 'btn-disabled' : '' }}">
                                 @if(empty($teacherId))
                                     请先选择老师
+                                @elseif(!empty($studentId))
+                                    @php
+                                        $selectedStudent = collect($this->students)->firstWhere('student_id', $studentId);
+                                    @endphp
+                                    {{ $selectedStudent ? trim($selectedStudent->name ?? $selectedStudent->student_id) . " ({$selectedStudent->grade} - {$selectedStudent->class_name})" : '请选择学生...' }}
                                 @else
                                     请选择学生...
                                 @endif
-                            </option>
-                            @foreach($this->students as $student)
-                                <option value="{{ $student->student_id }}">
-                                    {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
-                                </option>
-                            @endforeach
-                        </select>
+                                <svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
+                                </svg>
+                            </label>
+                            @if(!empty($teacherId))
+                                <ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full max-h-64 overflow-y-auto">
+                                    <li>
+                                        <a wire:click="$set('studentId', '')" class="{{ empty($studentId) ? 'active' : '' }}">
+                                            请选择学生...
+                                        </a>
+                                    </li>
+                                    @foreach($this->students as $student)
+                                        <li>
+                                            <a wire:click="$set('studentId', '{{ $student->student_id }}')" class="{{ $studentId === $student->student_id ? 'active' : '' }}">
+                                                {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
+                                            </a>
+                                        </li>
+                                    @endforeach
+                                </ul>
+                            @endif
+                        </div>
                         <p class="mt-1 text-xs text-gray-500">选择要查看的学生</p>
                     </div>
                 </div>