| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- <?php
- namespace App\Filament\Pages;
- 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 Livewire\Attributes\Computed;
- use Livewire\Attributes\On;
- use UnitEnum;
- class UploadExamPaper extends Page
- {
- use HasUserRole;
- 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 = 3;
- protected static ?string $slug = 'upload-exam-paper';
- protected string $view = 'filament.pages.upload-exam-paper';
- public ?string $teacherId = null;
- public ?string $studentId = null;
- // 模式选择
- public string $mode = 'select_paper'; // 'upload' 或 'select_paper'
- public ?string $selectedPaperId = null;
- public array $questions = [];
- public array $gradingData = [];
- 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->mode = 'select_paper';
- $this->selectedPaperId = null;
- $this->questionGrades = [];
- }
- #[Computed]
- public function teachers(): array
- {
- return app(ExamPaperService::class)->getTeachers(
- $this->isTeacher ? $this->getCurrentTeacherId() : null
- );
- }
- #[Computed]
- public function students(): array
- {
- return app(ExamPaperService::class)->getStudents($this->teacherId);
- }
- #[Computed]
- public function recentRecords(): array
- {
- return app(ExamPaperService::class)->getRecentRecords($this->studentId);
- }
- #[Computed]
- public function studentPapers(): array
- {
- return app(ExamPaperService::class)->getStudentPapers($this->studentId);
- }
- #[Computed]
- public function selectedPaperQuestions(): array
- {
- return app(ExamPaperService::class)->getPaperQuestions($this->selectedPaperId);
- }
- 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->selectedPaperId = null;
- $this->questionGrades = [];
- }
- /**
- * 处理评分完成事件
- */
- #[On('gradingComplete')]
- public function handleGradingComplete(): void
- {
- // 提交完成后跳转到详情页
- if ($this->selectedPaperId && $this->studentId) {
- $url = $this->getViewRecordUrl('graded_paper', $this->selectedPaperId, '', $this->studentId);
- $this->redirect($url, navigate: true);
- }
- }
- /**
- * 处理来自子组件的评分提交事件
- */
- #[On('submitManualGrading')]
- public function handleSubmitFromParent(array $questionGrades, array $gradingData, array $questions): void
- {
- // 从子组件接收数据
- $this->questionGrades = $questionGrades;
- $this->gradingData = $gradingData;
- $this->questions = $questions;
- \Log::info('UploadExamPaper: 接收到子组件提交的评分数据', [
- 'questionGrades_count' => count($questionGrades),
- 'questions_count' => count($questions)
- ]);
- // 调用原有的提交方法
- $this->submitManualGrading();
- // 通知用户提交成功
- Notification::make()
- ->title('评分提交成功')
- ->success()
- ->send();
- // 触发事件处理跳转
- $this->dispatch('gradingComplete');
- }
- #[On('teacherChanged')]
- public function updateTeacherId($teacherId)
- {
- $this->teacherId = $teacherId;
- $this->studentId = null;
- }
- #[On('studentChanged')]
- public function updateStudentId($teacherId, $studentId)
- {
- $this->studentId = $studentId;
- }
- /**
- * 提交手动评分
- */
- public function submitManualGrading(): void
- {
- if (!$this->selectedPaperId) {
- Notification::make()
- ->title('请选择试卷')
- ->danger()
- ->send();
- return;
- }
- // 将 gradingData 转换为 questionGrades 格式
- // 注意:这里假设子组件已经传递了处理好的 questionGrades,或者我们在这里再次处理
- // 如果子组件传递了 questionGrades,我们直接使用它。
- // 如果没有(比如直接在父组件调用),我们需要 convertGradingDataToQuestionGrades。
- // 但目前逻辑是子组件调用 handleSubmitFromParent 传递数据。
- 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) {
- // 从数据库获取当前题目的记录(包含满分)
- $paperQuestion = \App\Models\PaperQuestion::where('id', $questionId)->first();
- if (!$paperQuestion) {
- \Log::warning('未找到题目记录', ['question_id' => $questionId]);
- continue;
- }
- // 确保 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 的一致性
- // score 优先级高于 is_correct,根据得分比例动态计算
- $maxScore = $paperQuestion->score ?? 0;
- if ($maxScore > 0) {
- $scoreRatio = $score / $maxScore;
- // 只有达到满分才算完全正确
- if ($scoreRatio >= 1.0) {
- $isCorrect = true;
- } elseif ($scoreRatio > 0) {
- $isCorrect = false; // 部分得分不算完全正确
- } else {
- $isCorrect = false;
- }
- }
- \Log::info('保存评分数据', [
- 'question_id' => $questionId,
- 'max_score' => $maxScore,
- 'score_obtained' => $score,
- 'is_correct' => $isCorrect,
- 'score_ratio' => $maxScore > 0 ? ($score / $maxScore) : 0
- ]);
- \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 {
- // 构造 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;
- }
- // 计算满分
- $maxScore = floatval($question['score'] ?? 0);
- $scoreValue = floatval($grade['score'] ?? 0);
- $isCorrect = $maxScore > 0 ? ($scoreValue >= $maxScore) : ($grade['is_correct'] ?? false);
- $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' => $scoreValue,
- 'max_score' => $maxScore,
- 'is_correct' => $isCorrect,
- '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();
- }
- }
- /**
- * 查看记录详情 - 使用页面跳转
- */
- 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 '#';
- }
- }
|