| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196 |
- <?php
- namespace App\Filament\Pages;
- use App\Jobs\ProcessOCRRecord;
- use App\Models\OCRRecord;
- use App\Models\Student;
- use App\Models\Teacher;
- 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;
- protected static ?string $title = '上传试卷';
- protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cloud-arrow-up';
- protected static ?string $navigationLabel = '上传试卷';
- protected static string|UnitEnum|null $navigationGroup = '操作';
- protected static ?int $navigationSort = 2;
- protected static ?string $slug = 'upload-exam-paper';
- protected string $view = 'filament.pages.upload-exam-paper';
- 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()
- {
- // 初始化用户角色
- $this->initializeUserRole();
- // 如果是老师,自动选择当前老师
- if ($this->isTeacher) {
- $teacherId = $this->getCurrentTeacherId();
- if ($teacherId) {
- $this->teacherId = $teacherId;
- }
- } else {
- $this->teacherId = null;
- }
- $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 [];
- }
- }
- #[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 [];
- }
- }
- #[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 [];
- }
- }
- #[Computed]
- public function paperTypes(): array
- {
- return [
- '' => '请选择试卷形式',
- 'unit_test' => '单元测试',
- 'midterm' => '期中考试',
- 'final' => '期末考试',
- 'homework' => '家庭作业',
- 'quiz' => '随堂测验',
- 'other' => '其他',
- ];
- }
- /**
- * 获取选中试卷的题目列表
- */
- #[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
- ]
- ];
- }
- }
- public function updatedTeacherId($value): void
- {
- // 当教师选择变化时,清空之前选择的学生
- $this->studentId = null;
- $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 = [];
- }
- 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)
- {
- $this->teacherId = $teacherId;
- $this->studentId = null;
- }
- #[On('studentChanged')]
- public function updateStudentId($teacherId, $studentId)
- {
- $this->studentId = $studentId;
- }
- public function removeImage(): void
- {
- $this->uploadedImage = null;
- }
-
- /**
- * 提交手动评分
- */
- public function submitManualGrading(): void
- {
- if (!$this->selectedPaperId) {
- Notification::make()
- ->title('请选择试卷')
- ->danger()
- ->send();
- return;
- }
- // 将 gradingData 转换为 questionGrades 格式
- $this->convertGradingDataToQuestionGrades();
- if (empty($this->questionGrades)) {
- Notification::make()
- ->title('请至少为一道题目评分')
- ->danger()
- ->send();
- return;
- }
- try {
- // 准备数据发送到 LearningAnalytics
- $analyticsData = [];
-
- // 获取题目详情以便查找kp_code
- $questionsMap = collect($this->selectedPaperQuestions)->keyBy('id');
-
- // 收集需要从API补充信息的题目ID
- $missingKpCodeQuestionIds = [];
- foreach ($this->questionGrades as $questionId => $grade) {
- $question = $questionsMap->get($questionId);
- if (!$question) {
- continue;
- }
-
- // 优先使用本地存储的 kp_code
- if (empty($question['kp_code'])) {
- $missingKpCodeQuestionIds[] = $questionId;
- }
- }
-
- // 如果有缺失 kp_code 的题目,尝试从 API 获取
- $apiDetailsMap = collect([]);
- if (!empty($missingKpCodeQuestionIds)) {
- $questionBankIds = collect($missingKpCodeQuestionIds)
- ->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);
- $apiDetailsMap = collect($questionsDetails['data'] ?? [])->keyBy('id');
- }
- }
- foreach ($this->questionGrades as $questionId => $grade) {
- $question = $questionsMap->get($questionId);
- if (!$question) {
- continue;
- }
-
- $kpCode = $question['kp_code'];
-
- // 如果本地没有,尝试从API结果中获取
- if (empty($kpCode)) {
- $detail = $apiDetailsMap->get($question['question_bank_id']);
- $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null;
- }
- // 确保 is_correct 有值(如果为 null,设置为 false)
- $isCorrect = $grade['is_correct'];
- if ($isCorrect === null) {
- $isCorrect = false;
- }
- $analyticsData[] = [
- 'question_bank_id' => $question['question_bank_id'],
- 'student_answer' => $grade['student_answer'] ?? '',
- 'is_correct' => $isCorrect,
- 'score' => $grade['score'] ?? 0,
- 'max_score' => $question['score'] ?? 0,
- 'kp_code' => $kpCode,
- 'ip_address' => '127.0.0.1', // 提供默认IP地址,避免PostgreSQL inet类型错误
- 'device_type' => 'web', // 提供默认设备类型
- 'feedback_provided' => false, // 提供默认反馈状态
- ];
- }
- // 调用 LearningAnalytics 服务
- $learningAnalyticsService = app(\App\Services\LearningAnalyticsService::class);
- // 步骤0: 保存学生答案到本地数据库 (重要:确保数据持久化)
- foreach ($this->questionGrades as $questionId => $grade) {
- // 确保 is_correct 是布尔值(转换字符串 'true'/'false' 为布尔值)
- $isCorrect = $grade['is_correct'];
- if ($isCorrect === 'true' || $isCorrect === true) {
- $isCorrect = true;
- } elseif ($isCorrect === 'false' || $isCorrect === false) {
- $isCorrect = false;
- }
- // 确保 score_obtained 是数字
- $score = $grade['score'];
- if ($score !== null) {
- $score = is_numeric($score) ? (float)$score : 0;
- }
- // **关键修复**:确保 is_correct 和 score 的一致性
- // 如果分数为满分或大于0,视为正确
- if ($score > 0) {
- $isCorrect = true;
- } elseif ($score === 0) {
- $isCorrect = false;
- }
- \App\Models\PaperQuestion::where('id', $questionId)->update([
- 'student_answer' => $grade['student_answer'] ?? '',
- 'is_correct' => $isCorrect,
- 'score_obtained' => $score ?? 0,
- ]);
- }
- \Log::info('学生答案已保存到数据库', [
- 'student_id' => $this->studentId,
- 'paper_id' => $this->selectedPaperId,
- 'updated_count' => count($this->questionGrades)
- ]);
- // 步骤1: 保存答题记录到 LearningAnalytics
- \Log::info('准备调用submitBatchAttempts API', [
- 'student_id' => $this->studentId,
- 'paper_id' => $this->selectedPaperId,
- 'analytics_data_sample' => array_slice($analyticsData, 0, 2) // 记录前2题的数据作为样本
- ]);
- $result = $learningAnalyticsService->submitBatchAttempts($this->studentId, [
- 'paper_id' => $this->selectedPaperId,
- 'answers' => $analyticsData,
- ]);
- // 检查API返回结果
- if (is_array($result) && isset($result['error']) && $result['error']) {
- throw new \Exception($result['message'] ?? 'API调用失败');
- }
- if ($result === null || (is_array($result) && empty($result))) {
- throw new \Exception('API返回空数据');
- }
- \Log::info('答题记录已保存到学习分析服务', [
- 'student_id' => $this->studentId,
- 'paper_id' => $this->selectedPaperId,
- 'count' => count($analyticsData)
- ]);
- // 步骤2: 触发 AI 分析(包含掌握度更新和学习报告生成)
- try {
- $paper = \App\Models\Paper::find($this->selectedPaperId);
-
- // 构造 AI 分析请求数据
- $analysisQuestions = [];
- foreach ($this->questionGrades as $questionId => $grade) {
- $question = $questionsMap->get($questionId);
- 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'],
- 'question_text' => $question['content'] ?? '',
- 'student_answer' => $grade['student_answer'] ?? '',
- 'correct_answer' => $question['answer'] ?? '',
- 'kp_code' => $kpCode,
- 'score_value' => $grade['score'] ?? 0,
- 'max_score' => $question['score'],
- 'is_correct' => $grade['is_correct'] ?? false,
- 'teacher_validated' => true, // 手动评分即为教师验证
- 'ocr_confidence' => 1.0, // 手动评分置信度为1
- ];
- }
-
- $analysisData = [
- 'exam_id' => $this->selectedPaperId,
- 'student_id' => $this->studentId,
- 'ocr_record_id' => 0, // 系统生成卷子没有OCR记录ID
- 'paper_id' => $this->selectedPaperId,
- 'teacher_name' => auth()->user()->name ?? 'Teacher',
- 'analysis_type' => 'mastery',
- 'questions' => $analysisQuestions,
- ];
-
- // 调用统一的 AI 分析接口
- \Log::info('准备调用submitOCRAnalysis API', [
- 'paper_id' => $this->selectedPaperId,
- 'student_id' => $this->studentId,
- 'analysis_data_sample' => [
- 'question_count' => count($analysisQuestions),
- 'first_question' => $analysisQuestions[0] ?? null
- ]
- ]);
- $analysisResult = $learningAnalyticsService->submitOCRAnalysis($analysisData);
- \Log::info('AI分析已触发', [
- 'paper_id' => $this->selectedPaperId,
- 'student_id' => $this->studentId,
- '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分析失败', [
- 'paper_id' => $this->selectedPaperId,
- 'error' => $analysisError->getMessage()
- ]);
- }
- // 更新Paper表状态为已完成评分
- \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([
- 'status' => 'completed',
- 'completed_at' => now(),
- ]);
- Notification::make()
- ->title('提交成功')
- ->body('评分已提交,AI分析正在进行中')
- ->success()
- ->send();
- // 刷新最近记录列表
- unset($this->recentRecords);
- // 重置表单
- $this->selectedPaperId = null;
- $this->questionGrades = [];
- } catch (\Exception $e) {
- \Log::error('提交手动评分失败', [
- 'error' => $e->getMessage(),
- 'student_id' => $this->studentId,
- 'paper_id' => $this->selectedPaperId,
- ]);
- Notification::make()
- ->title('提交失败')
- ->body($e->getMessage())
- ->danger()
- ->send();
- }
- }
- /**
- * 将 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 && (
- $grading['is_correct'] !== null ||
- ($grading['score'] ?? null) !== null
- )) {
- $questionId = $question['id'];
- // 处理 is_correct 值(字符串 'true'/'false' 或布尔值)
- $isCorrect = $grading['is_correct'];
- if ($isCorrect === 'true') {
- $isCorrect = true;
- } elseif ($isCorrect === 'false') {
- $isCorrect = false;
- }
- // 处理 score 值
- $score = $grading['score'];
- 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) {
- $isCorrect = ($score >= ($question['score'] ?? 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());
- }
- }
- /**
- * 查看记录详情 - 使用页面跳转
- */
- public function getViewRecordUrl(string $type, string $paperId, string $recordId, string $studentId): string
- {
- // 返回ExamAnalysis详情页面URL
- if (in_array($type, ['graded_paper', 'generated'])) {
- // 系统生成或已评分试卷,使用paperId
- return '/admin/exam-analysis?paperId=' . $paperId . '&studentId=' . $studentId;
- } elseif ($type === 'ocr_upload') {
- // OCR上传记录,也跳转到详情页
- return '/admin/exam-analysis?recordId=' . $recordId . '&studentId=' . $studentId;
- }
- return '#';
- }
- }
|