loadTeachers(); // 从请求中获取老师ID或使用默认值 $this->teacherId = $request->input('teacher_id', $this->getDefaultTeacherId()); // 根据老师ID加载学生列表 $this->loadStudentsByTeacher(); // 从请求中获取学生ID或使用默认值 $this->studentId = $request->input('student_id', $this->getDefaultStudentId()); // 初始化知识点和技能数据 $this->loadKnowledgePointsAndSkills(); } /** * 获取默认老师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 : ''; } /** * 从MySQL加载老师列表 */ public function loadTeachers(): void { try { // 首先获取teachers表中的老师 $this->teachers = DB::connection('remote_mysql') ->table('teachers as t') ->leftJoin('users as u', 't.teacher_id', '=', 'u.user_id') ->select( 't.teacher_id', 't.name', 't.subject', 'u.username', 'u.email' ) ->orderBy('t.name') ->get() ->toArray(); // 如果有学生但没有对应的老师记录,添加一个"未知老师"条目 $teacherIds = array_column($this->teachers, 'teacher_id'); $missingTeacherIds = DB::connection('remote_mysql') ->table('students as s') ->distinct() ->whereNotIn('s.teacher_id', $teacherIds) ->pluck('teacher_id') ->toArray(); if (!empty($missingTeacherIds)) { foreach ($missingTeacherIds as $missingId) { $this->teachers[] = (object) [ 'teacher_id' => $missingId, 'name' => '未知老师 (' . $missingId . ')', 'subject' => '未知', 'username' => null, 'email' => null ]; } // 重新排序 usort($this->teachers, function($a, $b) { return strcmp($a->name, $b->name); }); } } catch (\Exception $e) { Log::error('加载老师列表失败', [ 'error' => $e->getMessage() ]); $this->teachers = []; } } /** * 根据老师ID加载学生列表 */ public function loadStudentsByTeacher(): void { try { if (empty($this->teacherId)) { $this->students = []; 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) ->select( 's.student_id', 's.name', 's.grade', 's.class_name', 'u.username', 'u.email' ) ->orderBy('s.name') ->get() ->toArray(); } catch (\Exception $e) { Log::error('加载学生列表失败', [ 'teacher_id' => $this->teacherId, 'error' => $e->getMessage() ]); $this->students = []; } } /** * 老师改变时重新加载学生列表 */ public function updatedTeacherId(): void { $this->loadStudentsByTeacher(); // 清空之前选中的学生ID $this->studentId = ''; // 自动加载第一个学生的数据 $this->studentId = $this->getDefaultStudentId(); if (!empty($this->studentId)) { $this->loadDashboardData(); } } public function loadDashboardData(): void { $this->isLoading = true; $this->errorMessage = ''; try { $service = new LearningAnalyticsService(); // 检查服务健康状态 if (!$service->checkHealth()) { $this->errorMessage = '学习分析系统当前不可用,请稍后重试'; $this->isLoading = false; return; } Log::info('开始加载仪表板数据', ['student_id' => $this->studentId]); // 获取各项数据 $masteryOverview = $service->getStudentMasteryOverview($this->studentId); $skillProficiency = $service->getStudentSkillProficiency($this->studentId); $skillSummary = $service->getStudentSkillSummary($this->studentId); $predictions = $service->getStudentPredictions($this->studentId, 5); $learningPaths = $service->getStudentLearningPaths($this->studentId, 3); $predictionAnalytics = $service->getPredictionAnalytics($this->studentId); $pathAnalytics = $service->getLearningPathAnalytics($this->studentId); $quickPrediction = $service->quickScorePrediction($this->studentId); Log::info('快速预测结果', [ 'student_id' => $this->studentId, 'quick_prediction' => $quickPrediction ]); $recommendations = $service->recommendLearningPaths($this->studentId, 3); // 组合数据 $this->dashboardData = [ 'mastery' => [ 'overview' => $masteryOverview, 'list' => $service->getStudentMasteryList($this->studentId), ], 'skill' => [ 'proficiency' => $skillProficiency, 'summary' => $skillSummary, ], 'prediction' => [ 'list' => $predictions, 'analytics' => $predictionAnalytics, 'quick' => $quickPrediction, ], 'learning_path' => [ 'list' => $learningPaths, 'analytics' => $pathAnalytics, 'recommendations' => $recommendations, ], ]; Log::info('仪表板数据加载完成', [ 'student_id' => $this->studentId, 'dashboard_data_keys' => array_keys($this->dashboardData) ]); } catch (\Exception $e) { $this->errorMessage = '加载数据时发生错误:' . $e->getMessage(); Log::error('学生仪表板数据加载失败', [ 'student_id' => $this->studentId, 'error' => $e->getMessage() ]); } finally { $this->isLoading = false; } } public function updatedStudentId(): void { // 学生ID更新后自动刷新数据 $this->loadDashboardData(); } public function recalculateMastery(string $kpCode): void { try { $service = new LearningAnalyticsService(); $result = $service->recalculateMastery($this->studentId, $kpCode); if ($result) { $this->dispatch('notify', message: '掌握度重新计算完成', type: 'success'); $this->loadDashboardData(); // 刷新数据 } else { $this->dispatch('notify', message: '掌握度重新计算失败', type: 'danger'); } } catch (\Exception $e) { Log::error('重新计算掌握度失败', [ 'student_id' => $this->studentId, 'kp_code' => $kpCode, 'error' => $e->getMessage() ]); $this->dispatch('notify', message: '操作失败:' . $e->getMessage(), type: 'danger'); } } public function batchUpdateSkills(): void { try { $service = new LearningAnalyticsService(); $result = $service->batchUpdateSkillProficiency($this->studentId); if ($result) { $this->dispatch('notify', message: '技能熟练度更新完成', type: 'success'); $this->loadDashboardData(); // 刷新数据 } else { $this->dispatch('notify', message: '技能熟练度更新失败', type: 'danger'); } } catch (\Exception $e) { Log::error('批量更新技能熟练度失败', [ 'student_id' => $this->studentId, 'error' => $e->getMessage() ]); $this->dispatch('notify', message: '操作失败:' . $e->getMessage(), type: 'danger'); } } public function generateQuickPrediction(): void { try { $service = new LearningAnalyticsService(); $result = $service->quickScorePrediction($this->studentId); if ($result) { $this->dispatch('notify', message: '快速预测生成完成', type: 'success'); $this->loadDashboardData(); // 刷新数据 } else { $this->dispatch('notify', message: '快速预测生成失败', type: 'danger'); } } catch (\Exception $e) { Log::error('生成快速预测失败', [ 'student_id' => $this->studentId, 'error' => $e->getMessage() ]); $this->dispatch('notify', message: '操作失败:' . $e->getMessage(), type: 'danger'); } } /** * 从题库API获取题目 */ private function fetchQuestionFromBank(): ?array { try { $questionBankApiBase = config('services.question_bank_api.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015')); // 调用题库API获取题目 $response = Http::timeout(10) ->get($questionBankApiBase . '/questions', [ 'limit' => 1, 'type' => 'factorization' ]); if ($response->successful()) { $data = $response->json(); $questions = $data['data'] ?? []; if (!empty($questions)) { $question = $questions[0]; $kpCode = $question['kp_code'] ?? null; $knowledgePointId = $question['knowledge_point_id'] ?? null; if ($kpCode && !$knowledgePointId) { $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode); } if (!$kpCode && $knowledgePointId) { $kpCode = $this->findKnowledgePointCodeById((string) $knowledgePointId); } if (!$knowledgePointId && $this->selectedKnowledgePoint) { $knowledgePointId = (string) $this->selectedKnowledgePoint; } if (!$kpCode && $this->selectedKnowledgePoint) { $kpCode = $this->findKnowledgePointCodeById((string) $this->selectedKnowledgePoint); } if (!$kpCode) { $kpCode = $this->getDefaultKnowledgePointMeta()['code']; } if (!$knowledgePointId && $kpCode) { $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode); } return [ 'id' => $question['question_code'] ?? 'Q_' . time(), 'content' => $question['stem'] ?? '', 'answer' => $question['solution'] ?? '', 'type' => '因式分解', 'difficulty' => $question['difficulty'] ?? rand(1, 5), 'kp_code' => $kpCode ?? 'KP_UNKNOWN', 'knowledge_point_id' => $knowledgePointId, 'skill' => $question['skill'] ?? 'unknown' ]; } } // 如果API失败,返回模拟题目 return $this->generateMockQuestion(); } catch (\Exception $e) { Log::warning('题库API调用失败,使用模拟题目', ['error' => $e->getMessage()]); return $this->generateMockQuestion(); } } /** * 生成模拟题目(备用方案) */ private function generateMockQuestion(): array { // 如果用户选择了知识点,使用选中的知识点的kp_code if ($this->selectedKnowledgePoint) { $selectedKpCode = null; foreach ($this->availableKnowledgePoints as $kp) { if ($kp['id'] === $this->selectedKnowledgePoint) { $selectedKpCode = $kp['code']; break; } } // 如果找到了选中的知识点的code,使用它 if ($selectedKpCode) { $mockQuestions = [ ['content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2], ['content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3], ['content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2], ['content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2], ['content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3], ]; $question = $mockQuestions[array_rand($mockQuestions)]; $question['type'] = '因式分解'; $question['kp_code'] = $selectedKpCode; $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint; return $question; } } // 如果没有选择知识点,使用默认的 $types = [ ['type' => '因式分解', 'kp_code' => 'KP7001', 'content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2], ['type' => '因式分解', 'kp_code' => 'KP8001', 'content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3], ['type' => '因式分解', 'kp_code' => 'KP8002', 'content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2], ['type' => '因式分解', 'kp_code' => 'KP8003', 'content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2], ['type' => '因式分解', 'kp_code' => 'KP8004', 'content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3], ]; $question = $types[array_rand($types)]; $kpMeta = $this->getDefaultKnowledgePointMeta(); $question['kp_code'] = $question['kp_code'] ?? 'KP_UNKNOWN'; $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']) ?? $kpMeta['id']; return $question; } /** * 从题库API获取多道题目 */ private function fetchMultipleQuestionsFromBank(int $count): array { try { $questionBankApiBase = config('services.question_bank_api.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015')); // 调用题库API一次性获取所有题目 // 如果用户选择了知识点,传入知识点参数 $params = [ 'limit' => $count, 'type' => 'factorization' ]; // 如果用户选择了知识点,传入kp_code if ($this->selectedKnowledgePoint) { foreach ($this->availableKnowledgePoints as $kp) { if ($kp['id'] === $this->selectedKnowledgePoint) { $params['kp_code'] = $kp['code']; break; } } } $response = Http::timeout(10) ->get($questionBankApiBase . '/questions', $params); if ($response->successful()) { $data = $response->json(); $questions = $data['data'] ?? []; $processedQuestions = []; foreach ($questions as $question) { $kpCode = $question['kp_code'] ?? null; $knowledgePointId = $question['knowledge_point_id'] ?? null; // 如果用户选择了知识点,优先使用选中的知识点 if ($this->selectedKnowledgePoint) { // 查找选中的知识点的kp_code $selectedKpCode = null; foreach ($this->availableKnowledgePoints as $kp) { if ($kp['id'] === $this->selectedKnowledgePoint) { $selectedKpCode = $kp['code']; break; } } if ($selectedKpCode) { $kpCode = $selectedKpCode; $knowledgePointId = (string) $this->selectedKnowledgePoint; } } else { // 如果没有选择知识点,使用API返回的或查找 if ($kpCode && !$knowledgePointId) { $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode); } if (!$kpCode && $knowledgePointId) { $kpCode = $this->findKnowledgePointCodeById((string) $knowledgePointId); } if (!$kpCode) { $kpCode = $this->getDefaultKnowledgePointMeta()['code']; } if (!$knowledgePointId && $kpCode) { $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode); } } $processedQuestions[] = [ 'id' => $question['question_code'] ?? 'Q_' . time() . '_' . uniqid(), 'content' => $question['stem'] ?? '', 'answer' => $question['solution'] ?? '', 'type' => '因式分解', 'difficulty' => $question['difficulty'] ?? rand(1, 5), 'kp_code' => $kpCode ?? 'KP_UNKNOWN', 'knowledge_point_id' => $knowledgePointId, 'skill' => $question['skill'] ?? 'unknown' ]; // 达到指定数量就停止处理 if (count($processedQuestions) >= $count) { break; } } // 如果API返回的题目不足,补充模拟题目 while (count($processedQuestions) < $count) { $mockQuestion = $this->generateMockQuestion(); // 重新生成唯一ID $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid(); $processedQuestions[] = $mockQuestion; } return $processedQuestions; } // 如果API失败,返回模拟题目 $mockQuestions = []; for ($i = 0; $i < $count; $i++) { $mockQuestion = $this->generateMockQuestion(); $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid(); $mockQuestions[] = $mockQuestion; } return $mockQuestions; } catch (\Exception $e) { Log::warning('题库API调用失败,使用模拟题目', ['error' => $e->getMessage()]); $mockQuestions = []; for ($i = 0; $i < $count; $i++) { $mockQuestion = $this->generateMockQuestion(); $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid(); $mockQuestions[] = $mockQuestion; } return $mockQuestions; } } /** * 根据答题结果更新掌握度(通过LearningAnalytics API) */ private function updateMasteryFromAnswer(array $question, bool $isCorrect): void { try { $learningAnalytics = new LearningAnalyticsService(); $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($question['knowledge_point_id'] ?? null); $attemptData = [ 'kp_code' => $kpCode ?? 'KP_UNKNOWN', 'is_correct' => $isCorrect, 'time_spent_seconds' => rand(60, 180), 'difficulty_level' => (string)($question['difficulty'] ?? '3'), 'question_id' => 'Q_' . time(), 'student_answer' => $this->userAnswer ?: '', 'correct_answer' => $question['answer'] ?? '', ]; if (!empty($question['knowledge_point_id'])) { $attemptData['knowledge_point_id'] = $question['knowledge_point_id']; } // 添加技能点数据(从题目中或当前选择的技能中获取) if (!empty($question['selected_skills'])) { $attemptData['skill_codes'] = $question['selected_skills']; } elseif (!empty($this->selectedSkills)) { $attemptData['skill_codes'] = $this->selectedSkills; } else { $attemptData['skill_codes'] = []; } $result = $learningAnalytics->submitAttempt($this->studentId, $attemptData); if (isset($result['error'])) { Log::error('LearningAnalytics API 调用失败', [ 'student_id' => $this->studentId, 'error' => $result['message'] ?? 'Unknown error', 'attempt_data' => $attemptData ]); } else { Log::info('答题记录已成功提交到 LearningAnalytics', [ 'student_id' => $this->studentId, 'attempt_id' => $result['attempt_id'] ?? null, 'mastery_level' => $result['mastery_level'] ?? null, 'knowledge_point_id' => $result['knowledge_point_id'] ?? null, 'skill_codes' => $result['skill_codes'] ?? [] ]); } } catch (\Exception $e) { Log::error('更新掌握度失败', [ 'student_id' => $this->studentId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); // 不再抛出异常,避免影响用户体验 } } /** * 加载知识点和技能数据 */ public function loadKnowledgePointsAndSkills(): void { try { $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011')); // 从知识图谱API获取知识点数据 $kpResponse = Http::timeout(10) ->get($knowledgeApiBase . '/knowledge-points/', [ 'page' => 1, 'per_page' => 100 ]); if ($kpResponse->successful()) { $kpData = $kpResponse->json(); $this->availableKnowledgePoints = $kpData['data'] ?? $kpData ?? []; // 格式化知识点数据,确保包含必要的字段 $this->availableKnowledgePoints = array_map(function($kp) { return [ 'id' => (string)($kp['id'] ?? $kp['kp_id'] ?? uniqid()), 'code' => $kp['kp_code'] ?? $kp['kp_id'] ?? $kp['code'] ?? 'KP_UNKNOWN', 'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kp['name'] ?? $kp['kp_code'] ?? '未知知识点', 'subject' => $kp['category'] ?? '数学' ]; }, $this->availableKnowledgePoints); } else { throw new \Exception('知识图谱API调用失败: ' . $kpResponse->status()); } // 从知识图谱API获取技能数据 $skillResponse = Http::timeout(10) ->get($knowledgeApiBase . '/skills/', [ 'page' => 1, 'per_page' => 50 ]); if ($skillResponse->successful()) { $skillData = $skillResponse->json(); $this->availableSkills = $skillData['data'] ?? $skillData ?? []; // 格式化技能数据 $this->availableSkills = array_map(function($skill) { return [ 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()), 'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN', 'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能', 'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能' ]; }, $this->availableSkills); } else { throw new \Exception('技能API调用失败: ' . $skillResponse->status()); } Log::info('成功从知识图谱API加载数据', [ 'knowledge_points_count' => count($this->availableKnowledgePoints), 'skills_count' => count($this->availableSkills) ]); } catch (\Exception $e) { Log::error('从知识图谱API加载知识点和技能数据失败,使用备用数据', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); // 使用模拟数据作为备用 $this->availableKnowledgePoints = [ ['id' => 'factor_1', 'code' => 'factor_1', 'name' => '因式分解基础', 'subject' => '数学'], ['id' => 'factor_2', 'code' => 'factor_2', 'name' => '提取公因式', 'subject' => '数学'], ['id' => 'factor_3', 'code' => 'factor_3', 'name' => '平方差公式', 'subject' => '数学'], ['id' => 'factor_4', 'code' => 'factor_4', 'name' => '完全平方公式', 'subject' => '数学'], ['id' => 'factor_5', 'code' => 'factor_5', 'name' => '分组分解法', 'subject' => '数学'], ['id' => 'factor_6', 'code' => 'factor_6', 'name' => '立方和差公式', 'subject' => '数学'], ['id' => 'factor_7', 'code' => 'factor_7', 'name' => '十字相乘法', 'subject' => '数学'], ['id' => 'factor_8', 'code' => 'factor_8', 'name' => '综合因式分解', 'subject' => '数学'], ]; $this->availableSkills = [ ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'], ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'], ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'], ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'], ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'], ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'], ]; } $this->buildKnowledgePointIndexes(); // 不在这里初始化联动,让用户手动选择后再加载 // 避免在页面加载时就调用API Log::info('知识点和技能数据加载完成'); } /** * 为知识点构建索引映射 */ private function buildKnowledgePointIndexes(): void { $this->knowledgePointCodeIndex = []; $this->knowledgePointIdIndex = []; foreach ($this->availableKnowledgePoints as $kp) { // 使用格式化后的字段:code 对应 kp_code if (!empty($kp['code'])) { $this->knowledgePointCodeIndex[(string) $kp['code']] = $kp; } // 使用格式化后的字段:id if (!empty($kp['id'])) { $this->knowledgePointIdIndex[(string) $kp['id']] = $kp; } } } /** * 将技能ID数组转换为技能名称数组 */ private function convertSkillIdsToNames(array $skillIds): array { $skillNames = []; foreach ($skillIds as $skillId) { foreach ($this->availableSkills as $skill) { if ((string)$skill['id'] === (string)$skillId || (string)$skill['code'] === (string)$skillId) { $skillNames[] = $skill['name']; break; } } } return $skillNames; } /** * 通过 ID 或 code 查找知识点 */ private function findKnowledgePointByIdOrCode(?string $identifier): ?array { if (empty($identifier)) { return null; } if (isset($this->knowledgePointIdIndex[$identifier])) { return $this->knowledgePointIdIndex[$identifier]; } if (isset($this->knowledgePointCodeIndex[$identifier])) { return $this->knowledgePointCodeIndex[$identifier]; } return $this->findKnowledgePointByCode($identifier); } /** * 通过知识点 code 获取详情 */ private function findKnowledgePointByCode(?string $kpCode): ?array { if (empty($kpCode)) { return null; } if (isset($this->knowledgePointCodeIndex[$kpCode])) { return $this->knowledgePointCodeIndex[$kpCode]; } $fetched = $this->fetchKnowledgePointFromApi($kpCode); if ($fetched) { $this->availableKnowledgePoints[] = $fetched; $this->buildKnowledgePointIndexes(); } return $fetched; } /** * 调用知识图谱 API 获取知识点 */ private function fetchKnowledgePointFromApi(string $kpCode): ?array { try { $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011')); $response = Http::timeout(10)->get($knowledgeApiBase . '/knowledge-points/' . $kpCode); if ($response->successful()) { $kp = $response->json(); return [ 'id' => (string) ($kp['id'] ?? $kp['kp_id'] ?? $kpCode), 'code' => $kp['kp_code'] ?? $kpCode, 'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kpCode, 'subject' => $kp['category'] ?? '数学' ]; } } catch (\Exception $e) { Log::warning('获取知识点详情失败', [ 'kp_code' => $kpCode, 'error' => $e->getMessage() ]); } return null; } /** * 获取知识点 ID(根据 code) */ private function findKnowledgePointIdByCode(?string $kpCode): ?string { $kp = $this->findKnowledgePointByCode($kpCode); if (!$kp || empty($kp['id'])) { return null; } return (string) $kp['id']; } /** * 根据 ID 获取知识点 code */ private function findKnowledgePointCodeById(?string $kpId): ?string { $kp = $this->findKnowledgePointByIdOrCode($kpId); return $kp['code'] ?? null; } /** * 获取一个默认的知识点(用于兜底数据) */ private function getDefaultKnowledgePointMeta(): array { if (!empty($this->availableKnowledgePoints)) { $kp = $this->availableKnowledgePoints[array_rand($this->availableKnowledgePoints)]; return [ 'id' => isset($kp['id']) ? (string) $kp['id'] : null, 'code' => $kp['code'] ?? null ]; } return [ 'id' => null, 'code' => 'KP_UNKNOWN' ]; } /** * 知识点选择变化时更新技能列表 */ public function updatedSelectedKnowledgePoint(): void { // 清空已选择的技能 $this->selectedSkills = []; // 如果没有选择知识点,加载所有技能 if (empty($this->selectedKnowledgePoint)) { $this->loadAllSkills(); return; } // 根据选择的知识点获取相关技能 $this->loadSkillsForKnowledgePoint($this->selectedKnowledgePoint); } /** * 根据知识点加载相关技能 */ private function loadSkillsForKnowledgePoint(string $knowledgePointId): void { $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011')); // 根据knowledgePointId查找对应的kp_code $kpCode = null; foreach ($this->availableKnowledgePoints as $kp) { if ($kp['id'] === $knowledgePointId) { $kpCode = $kp['code']; // 使用kp_code作为API参数 break; } } if (!$kpCode) { Log::warning('未找到知识点对应的kp_code', ['knowledge_point_id' => $knowledgePointId]); $this->availableSkills = []; return; } Log::info('准备调用知识点详情API', [ 'kp_code' => $kpCode, 'knowledge_point_id' => $knowledgePointId ]); // 直接从知识点详情API获取技能列表 $kpDetailResponse = Http::timeout(10) ->get($knowledgeApiBase . '/knowledge-points/' . $kpCode); $kpData = $kpDetailResponse->json(); // 打印完整响应,方便调试 Log::info('知识点API完整响应', [ 'knowledge_point' => $kpCode, 'status' => $kpDetailResponse->status(), 'response' => $kpData ]); // 转换技能数据格式,匹配模板期望的字段名 $skills = $kpData['skills'] ?? []; $this->availableSkills = array_map(function($skill) { return [ 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? ''), 'code' => $skill['skill_code'] ?? '', 'name' => $skill['skill_name'] ?? '', 'category' => $skill['skill_type'] ?? '' ]; }, $skills); Log::info('设置技能列表', [ 'count' => count($this->availableSkills), 'skills' => $this->availableSkills ]); } /** * 加载所有技能 */ private function loadAllSkills(): void { // 先保存当前技能列表作为兜底 $fallbackSkills = $this->availableSkills; try { $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011')); $skillResponse = Http::timeout(10) ->get($knowledgeApiBase . '/skills/', [ 'page' => 1, 'per_page' => 50 ]); if ($skillResponse->successful()) { $skillData = $skillResponse->json(); $skills = $skillData['data'] ?? $skillData ?? []; // 只有当API返回有效数据时才更新技能列表 if (!empty($skills) && is_array($skills)) { $this->availableSkills = array_map(function($skill) { return [ 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()), 'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN', 'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能', 'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能' ]; }, $skills); Log::info('成功加载所有技能', [ 'skills_count' => count($this->availableSkills) ]); return; } } // 如果API调用失败或返回空数据,使用默认技能列表 Log::warning('加载所有技能失败或为空,使用默认技能列表'); $this->useDefaultSkills(); } catch (\Exception $e) { Log::error('加载所有技能失败,使用默认技能列表', ['error' => $e->getMessage()]); $this->useDefaultSkills(); } } /** * 使用默认技能列表 */ private function useDefaultSkills(): void { $this->availableSkills = [ ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'], ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'], ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'], ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'], ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'], ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'], ]; } /** * 生成批量题目 */ public function generateBatchQuestions(): void { if (empty($this->studentId)) { $this->dispatch('notify', message: '请先选择学生', type: 'warning'); return; } try { $this->isLoading = true; // 生成批次ID $this->currentBatchId = 'BATCH_' . $this->studentId . '_' . time(); // 一次性获取所有需要的题目,避免重复调用API $allQuestions = $this->fetchMultipleQuestionsFromBank($this->questionsPerSet); // 处理题目 $questions = []; foreach ($allQuestions as $question) { if (empty($question['knowledge_point_id']) && !empty($question['kp_code'])) { $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']); } if (empty($question['knowledge_point_id']) && $this->selectedKnowledgePoint) { $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint; } // 添加选择的知识点和技能信息 $question['batch_id'] = $this->currentBatchId; $question['selected_knowledge_point'] = $this->selectedKnowledgePoint; $question['selected_skills'] = $this->selectedSkills; $questions[] = $question; } if (!empty($questions)) { $this->exerciseQuestions = $questions; // 初始化答题数组 $this->exerciseAnswers = []; foreach ($questions as $index => $question) { $this->exerciseAnswers[$index] = [ 'user_answer' => '', 'is_correct' => null, ]; } $this->dispatch('notify', message: "成功生成 {$this->questionsPerSet} 道题目", type: 'success'); } else { $this->dispatch('notify', message: '生成题目失败,请重试', type: 'danger'); } } catch (\Exception $e) { Log::error('生成批量题目失败', [ 'student_id' => $this->studentId, 'questions_count' => $this->questionsPerSet, 'error' => $e->getMessage() ]); $this->dispatch('notify', message: '生成题目失败:' . $e->getMessage(), type: 'danger'); } finally { $this->isLoading = false; } } /** * 批量提交答案 */ public function submitBatchAnswers(): void { if (empty($this->studentId) || empty($this->exerciseQuestions) || empty($this->currentBatchId)) { $this->dispatch('notify', message: '没有可提交的题目', type: 'warning'); return; } try { $this->isLoading = true; $successCount = 0; $failureCount = 0; foreach ($this->exerciseQuestions as $index => $question) { $answer = $this->exerciseAnswers[$index] ?? null; if (!$answer || $answer['is_correct'] === null) { continue; // 跳过未答题的题目 } try { // 确保knowledge_point_id始终是数字ID $knowledgePointId = $question['knowledge_point_id'] ?? null; // 如果knowledge_point_id是code,转换为ID if ($knowledgePointId && !is_numeric($knowledgePointId)) { $knowledgePointId = $this->findKnowledgePointIdByCode($knowledgePointId); } // 如果还是没有,使用选中的知识点 if (!$knowledgePointId && $this->selectedKnowledgePoint) { $selectedValue = $this->selectedKnowledgePoint; // 如果选中的是code,转换为ID;如果是ID,直接使用 if (!is_numeric($selectedValue)) { $knowledgePointId = $this->findKnowledgePointIdByCode($selectedValue); } else { $knowledgePointId = (string)$selectedValue; } } // 如果还是没有,从题目kp_code查找 if (!$knowledgePointId && !empty($question['kp_code'])) { $knowledgePointId = $this->findKnowledgePointIdByCode($question['kp_code']); } $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($knowledgePointId) ?? 'KP_UNKNOWN'; // 准备数据库存储数据 $exerciseData = [ 'student_id' => $this->studentId, 'question_id' => $question['id'] ?? 'Q_' . $this->currentBatchId . '_' . $index, // 确保knowledge_point_id是整数或null,不能是字符串 'knowledge_point_id' => is_numeric($knowledgePointId) ? (int)$knowledgePointId : null, 'question_content' => $question['content'] ?? '', 'student_answer' => $answer['user_answer'] ?? '', 'correct_answer' => $question['answer'] ?? '', 'is_correct' => $answer['is_correct'], 'submission_status' => 'submitted', 'batch_id' => $this->currentBatchId, 'kp_code' => $kpCode, 'selected_skills' => json_encode($this->selectedSkills), 'skill_scores' => $this->calculateSkillScores($this->selectedSkills, $answer['is_correct']), 'time_spent_seconds' => rand(60, 180), 'difficulty_level' => is_numeric($question['difficulty'] ?? 3) ? (float)$question['difficulty'] : 3, 'created_at' => now(), 'updated_at' => now(), ]; // 保存到 Laravel 数据库 \App\Models\StudentExercise::create($exerciseData); // 提交给 LearningAnalytics 系统 $this->updateMasteryFromBatchAnswer($question, $answer['is_correct']); $successCount++; } catch (\Exception $e) { Log::error('批量答题中的单题提交失败', [ 'student_id' => $this->studentId, 'question_index' => $index, 'error' => $e->getMessage() ]); $failureCount++; } } // 清空批量数据 $this->exerciseQuestions = []; $this->exerciseAnswers = []; $this->currentBatchId = ''; // 提交结果 $totalQuestions = $successCount + $failureCount; $this->dispatch('notify', message: "批量提交完成!成功: {$successCount} 题,失败: {$failureCount} 题", type: $failureCount === 0 ? 'success' : 'warning' ); // 刷新仪表板数据 $this->loadDashboardData(); // 批量更新技能熟练度 try { $learningAnalytics = new LearningAnalyticsService(); $skillResult = $learningAnalytics->batchUpdateSkillProficiency($this->studentId); if ($skillResult) { Log::info('技能熟练度批量更新成功', ['student_id' => $this->studentId]); } } catch (\Exception $e) { Log::warning('技能熟练度批量更新失败(不影响答题提交)', [ 'student_id' => $this->studentId, 'error' => $e->getMessage() ]); } } catch (\Exception $e) { Log::error('批量提交答案失败', [ 'student_id' => $this->studentId, 'batch_id' => $this->currentBatchId, 'error' => $e->getMessage() ]); $this->dispatch('notify', message: '批量提交失败:' . $e->getMessage(), type: 'danger'); } finally { $this->isLoading = false; } } /** * 计算技能评分 */ private function calculateSkillScores(array $selectedSkills, bool $isCorrect): string { if (empty($selectedSkills)) { return json_encode([]); } $scores = []; $baseScore = $isCorrect ? 0.8 : 0.2; // 正确答对给0.8分,错误给0.2分 $randomFactor = rand(-10, 10) / 100; // 添加随机因素 foreach ($selectedSkills as $skillId) { $score = max(0, min(1, $baseScore + $randomFactor)); $scores[$skillId] = round($score, 3); } return json_encode($scores); } /** * 批量答题时更新掌握度 */ private function updateMasteryFromBatchAnswer(array $question, bool $isCorrect): void { try { $learningAnalytics = new LearningAnalyticsService(); $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($question['knowledge_point_id'] ?? null); $attemptData = [ 'kp_code' => $kpCode ?? 'KP_UNKNOWN', 'is_correct' => $isCorrect, 'time_spent_seconds' => rand(60, 180), 'difficulty_level' => (string)($question['difficulty'] ?? '3'), 'question_id' => 'Q_' . $this->currentBatchId . '_' . rand(1000, 9999), 'student_answer' => '', 'correct_answer' => $question['answer'] ?? '', ]; if (!empty($question['knowledge_point_id'])) { $attemptData['knowledge_point_id'] = $question['knowledge_point_id']; } // 添加技能点数据(使用技能ID,ID是唯一的) if (!empty($question['selected_skills'])) { $attemptData['skill_codes'] = $question['selected_skills']; } elseif (!empty($this->selectedSkills)) { $attemptData['skill_codes'] = $this->selectedSkills; } else { $attemptData['skill_codes'] = []; } $result = $learningAnalytics->submitAttempt($this->studentId, $attemptData); if (isset($result['error'])) { Log::error('LearningAnalytics API 调用失败', [ 'student_id' => $this->studentId, 'batch_id' => $this->currentBatchId, 'error' => $result['message'] ?? 'Unknown error', 'attempt_data' => $attemptData ]); } else { Log::info('批量答题记录已成功提交', [ 'student_id' => $this->studentId, 'batch_id' => $this->currentBatchId, 'attempt_id' => $result['attempt_id'] ?? null, 'knowledge_point_id' => $result['knowledge_point_id'] ?? null, 'skill_codes' => $result['skill_codes'] ?? [] ]); } } catch (\Exception $e) { Log::error('更新批量答题掌握度失败', [ 'student_id' => $this->studentId, 'batch_id' => $this->currentBatchId, 'error' => $e->getMessage() ]); } } /** * 清除历史记录 */ public function clearHistory(): void { $this->questionHistory = []; $this->dispatch('notify', message: '历史记录已清除', type: 'info'); } /** * 清空学生的所有答题数据 */ public function clearStudentAllData(): void { if (empty($this->studentId)) { $this->dispatch('notify', message: '请先选择学生', type: 'warning'); return; } try { $this->isLoading = true; $service = new LearningAnalyticsService(); $result = $service->clearStudentData($this->studentId); if ($result) { $this->dispatch('notify', message: '学生答题数据已清空', type: 'success'); // 清空当前仪表板数据 $this->dashboardData = []; // 重新加载仪表板数据 $this->loadDashboardData(); } else { $this->dispatch('notify', message: '清空数据时发生错误,请检查日志', type: 'danger'); } } catch (\Exception $e) { Log::error('清空学生数据失败', [ 'student_id' => $this->studentId, 'error' => $e->getMessage() ]); $this->dispatch('notify', message: '清空数据失败:' . $e->getMessage(), type: 'danger'); } finally { $this->isLoading = false; } } }