|
|
@@ -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());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* 查看记录详情 - 使用页面跳转
|
|
|
*/
|