|
|
@@ -6,6 +6,8 @@ use App\Services\LearningAnalyticsService;
|
|
|
use BackedEnum;
|
|
|
use Filament\Pages\Page;
|
|
|
use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
use UnitEnum;
|
|
|
use Livewire\Attributes\Layout;
|
|
|
@@ -28,14 +30,161 @@ class StudentDashboard extends Page
|
|
|
protected string $view = 'filament.pages.student-dashboard';
|
|
|
|
|
|
public string $studentId = '';
|
|
|
+ public string $teacherId = '';
|
|
|
public array $dashboardData = [];
|
|
|
public bool $isLoading = false;
|
|
|
public string $errorMessage = '';
|
|
|
+ public array $teachers = [];
|
|
|
+ public array $students = [];
|
|
|
+
|
|
|
+ // 批量答题相关
|
|
|
+ public array $exerciseQuestions = [];
|
|
|
+ public array $exerciseAnswers = [];
|
|
|
+ public ?string $selectedKnowledgePoint = null;
|
|
|
+ public array $availableKnowledgePoints = [];
|
|
|
+ public array $availableSkills = [];
|
|
|
+ public array $selectedSkills = [];
|
|
|
+ public string $currentBatchId = '';
|
|
|
+ public int $questionsPerSet = 5;
|
|
|
+ protected array $knowledgePointCodeIndex = [];
|
|
|
+ protected array $knowledgePointIdIndex = [];
|
|
|
|
|
|
public function mount(Request $request): void
|
|
|
{
|
|
|
+ // 加载老师列表
|
|
|
+ $this->loadTeachers();
|
|
|
+
|
|
|
+ // 从请求中获取老师ID或使用默认值
|
|
|
+ $this->teacherId = $request->input('teacher_id', $this->getDefaultTeacherId());
|
|
|
+
|
|
|
+ // 根据老师ID加载学生列表
|
|
|
+ $this->loadStudentsByTeacher();
|
|
|
+
|
|
|
// 从请求中获取学生ID或使用默认值
|
|
|
- $this->studentId = $request->input('student_id', 'student_001');
|
|
|
+ $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
|
|
|
@@ -53,6 +202,8 @@ class StudentDashboard extends Page
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ Log::info('开始加载仪表板数据', ['student_id' => $this->studentId]);
|
|
|
+
|
|
|
// 获取各项数据
|
|
|
$masteryOverview = $service->getStudentMasteryOverview($this->studentId);
|
|
|
$skillProficiency = $service->getStudentSkillProficiency($this->studentId);
|
|
|
@@ -62,6 +213,12 @@ class StudentDashboard extends Page
|
|
|
$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);
|
|
|
|
|
|
// 组合数据
|
|
|
@@ -86,6 +243,11 @@ class StudentDashboard extends Page
|
|
|
],
|
|
|
];
|
|
|
|
|
|
+ Log::info('仪表板数据加载完成', [
|
|
|
+ 'student_id' => $this->studentId,
|
|
|
+ 'dashboard_data_keys' => array_keys($this->dashboardData)
|
|
|
+ ]);
|
|
|
+
|
|
|
} catch (\Exception $e) {
|
|
|
$this->errorMessage = '加载数据时发生错误:' . $e->getMessage();
|
|
|
Log::error('学生仪表板数据加载失败', [
|
|
|
@@ -166,4 +328,1006 @@ class StudentDashboard extends Page
|
|
|
$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;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|