StudentDashboard.php 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Services\LearningAnalyticsService;
  4. use BackedEnum;
  5. use Filament\Pages\Page;
  6. use Illuminate\Http\Request;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Http;
  9. use Illuminate\Support\Facades\Log;
  10. use UnitEnum;
  11. use Livewire\Attributes\Layout;
  12. use Livewire\Attributes\Title;
  13. class StudentDashboard extends Page
  14. {
  15. use \Filament\Pages\Concerns\InteractsWithFormActions;
  16. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
  17. protected static string|UnitEnum|null $navigationGroup = '学习分析';
  18. protected static ?string $navigationLabel = '学生仪表板';
  19. protected static ?int $navigationSort = 1;
  20. protected ?string $heading = '学生仪表板';
  21. protected string $view = 'filament.pages.student-dashboard';
  22. public string $studentId = '';
  23. public string $teacherId = '';
  24. public array $dashboardData = [];
  25. public bool $isLoading = false;
  26. public string $errorMessage = '';
  27. public array $teachers = [];
  28. public array $students = [];
  29. // 批量答题相关
  30. public array $exerciseQuestions = [];
  31. public array $exerciseAnswers = [];
  32. public ?string $selectedKnowledgePoint = null;
  33. public array $availableKnowledgePoints = [];
  34. public array $availableSkills = [];
  35. public array $selectedSkills = [];
  36. public string $currentBatchId = '';
  37. public int $questionsPerSet = 5;
  38. protected array $knowledgePointCodeIndex = [];
  39. protected array $knowledgePointIdIndex = [];
  40. public function mount(Request $request): void
  41. {
  42. // 加载老师列表
  43. $this->loadTeachers();
  44. // 从请求中获取老师ID或使用默认值
  45. $this->teacherId = $request->input('teacher_id', $this->getDefaultTeacherId());
  46. // 根据老师ID加载学生列表
  47. $this->loadStudentsByTeacher();
  48. // 从请求中获取学生ID或使用默认值
  49. $this->studentId = $request->input('student_id', $this->getDefaultStudentId());
  50. // 初始化知识点和技能数据
  51. $this->loadKnowledgePointsAndSkills();
  52. }
  53. /**
  54. * 获取默认老师ID(列表中的第一个老师)
  55. */
  56. private function getDefaultTeacherId(): string
  57. {
  58. return !empty($this->teachers) ? $this->teachers[0]->teacher_id : '';
  59. }
  60. /**
  61. * 获取默认学生ID(列表中的第一个学生)
  62. */
  63. private function getDefaultStudentId(): string
  64. {
  65. return !empty($this->students) ? $this->students[0]->student_id : '';
  66. }
  67. /**
  68. * 从MySQL加载老师列表
  69. */
  70. public function loadTeachers(): void
  71. {
  72. try {
  73. // 首先获取teachers表中的老师
  74. $this->teachers = DB::connection('remote_mysql')
  75. ->table('teachers as t')
  76. ->leftJoin('users as u', 't.teacher_id', '=', 'u.user_id')
  77. ->select(
  78. 't.teacher_id',
  79. 't.name',
  80. 't.subject',
  81. 'u.username',
  82. 'u.email'
  83. )
  84. ->orderBy('t.name')
  85. ->get()
  86. ->toArray();
  87. // 如果有学生但没有对应的老师记录,添加一个"未知老师"条目
  88. $teacherIds = array_column($this->teachers, 'teacher_id');
  89. $missingTeacherIds = DB::connection('remote_mysql')
  90. ->table('students as s')
  91. ->distinct()
  92. ->whereNotIn('s.teacher_id', $teacherIds)
  93. ->pluck('teacher_id')
  94. ->toArray();
  95. if (!empty($missingTeacherIds)) {
  96. foreach ($missingTeacherIds as $missingId) {
  97. $this->teachers[] = (object) [
  98. 'teacher_id' => $missingId,
  99. 'name' => '未知老师 (' . $missingId . ')',
  100. 'subject' => '未知',
  101. 'username' => null,
  102. 'email' => null
  103. ];
  104. }
  105. // 重新排序
  106. usort($this->teachers, function($a, $b) {
  107. return strcmp($a->name, $b->name);
  108. });
  109. }
  110. } catch (\Exception $e) {
  111. Log::error('加载老师列表失败', [
  112. 'error' => $e->getMessage()
  113. ]);
  114. $this->teachers = [];
  115. }
  116. }
  117. /**
  118. * 根据老师ID加载学生列表
  119. */
  120. public function loadStudentsByTeacher(): void
  121. {
  122. try {
  123. if (empty($this->teacherId)) {
  124. $this->students = [];
  125. return;
  126. }
  127. $this->students = DB::connection('remote_mysql')
  128. ->table('students as s')
  129. ->leftJoin('users as u', 's.student_id', '=', 'u.user_id')
  130. ->where('s.teacher_id', $this->teacherId)
  131. ->select(
  132. 's.student_id',
  133. 's.name',
  134. 's.grade',
  135. 's.class_name',
  136. 'u.username',
  137. 'u.email'
  138. )
  139. ->orderBy('s.name')
  140. ->get()
  141. ->toArray();
  142. } catch (\Exception $e) {
  143. Log::error('加载学生列表失败', [
  144. 'teacher_id' => $this->teacherId,
  145. 'error' => $e->getMessage()
  146. ]);
  147. $this->students = [];
  148. }
  149. }
  150. /**
  151. * 老师改变时重新加载学生列表
  152. */
  153. public function updatedTeacherId(): void
  154. {
  155. $this->loadStudentsByTeacher();
  156. // 清空之前选中的学生ID
  157. $this->studentId = '';
  158. // 自动加载第一个学生的数据
  159. $this->studentId = $this->getDefaultStudentId();
  160. if (!empty($this->studentId)) {
  161. $this->loadDashboardData();
  162. }
  163. }
  164. public function loadDashboardData(): void
  165. {
  166. $this->isLoading = true;
  167. $this->errorMessage = '';
  168. try {
  169. $service = new LearningAnalyticsService();
  170. // 检查服务健康状态
  171. if (!$service->checkHealth()) {
  172. $this->errorMessage = '学习分析系统当前不可用,请稍后重试';
  173. $this->isLoading = false;
  174. return;
  175. }
  176. Log::info('开始加载仪表板数据', ['student_id' => $this->studentId]);
  177. // 获取各项数据
  178. $masteryOverview = $service->getStudentMasteryOverview($this->studentId);
  179. $skillProficiency = $service->getStudentSkillProficiency($this->studentId);
  180. $skillSummary = $service->getStudentSkillSummary($this->studentId);
  181. $predictions = $service->getStudentPredictions($this->studentId, 5);
  182. $learningPaths = $service->getStudentLearningPaths($this->studentId, 3);
  183. $predictionAnalytics = $service->getPredictionAnalytics($this->studentId);
  184. $pathAnalytics = $service->getLearningPathAnalytics($this->studentId);
  185. $quickPrediction = $service->quickScorePrediction($this->studentId);
  186. Log::info('快速预测结果', [
  187. 'student_id' => $this->studentId,
  188. 'quick_prediction' => $quickPrediction
  189. ]);
  190. $recommendations = $service->recommendLearningPaths($this->studentId, 3);
  191. // 组合数据
  192. $this->dashboardData = [
  193. 'mastery' => [
  194. 'overview' => $masteryOverview,
  195. 'list' => $service->getStudentMasteryList($this->studentId),
  196. ],
  197. 'skill' => [
  198. 'proficiency' => $skillProficiency,
  199. 'summary' => $skillSummary,
  200. ],
  201. 'prediction' => [
  202. 'list' => $predictions,
  203. 'analytics' => $predictionAnalytics,
  204. 'quick' => $quickPrediction,
  205. ],
  206. 'learning_path' => [
  207. 'list' => $learningPaths,
  208. 'analytics' => $pathAnalytics,
  209. 'recommendations' => $recommendations,
  210. ],
  211. ];
  212. Log::info('仪表板数据加载完成', [
  213. 'student_id' => $this->studentId,
  214. 'dashboard_data_keys' => array_keys($this->dashboardData)
  215. ]);
  216. } catch (\Exception $e) {
  217. $this->errorMessage = '加载数据时发生错误:' . $e->getMessage();
  218. Log::error('学生仪表板数据加载失败', [
  219. 'student_id' => $this->studentId,
  220. 'error' => $e->getMessage()
  221. ]);
  222. } finally {
  223. $this->isLoading = false;
  224. }
  225. }
  226. public function updatedStudentId(): void
  227. {
  228. // 学生ID更新后自动刷新数据
  229. $this->loadDashboardData();
  230. }
  231. public function recalculateMastery(string $kpCode): void
  232. {
  233. try {
  234. $service = new LearningAnalyticsService();
  235. $result = $service->recalculateMastery($this->studentId, $kpCode);
  236. if ($result) {
  237. $this->dispatch('notify', message: '掌握度重新计算完成', type: 'success');
  238. $this->loadDashboardData(); // 刷新数据
  239. } else {
  240. $this->dispatch('notify', message: '掌握度重新计算失败', type: 'danger');
  241. }
  242. } catch (\Exception $e) {
  243. Log::error('重新计算掌握度失败', [
  244. 'student_id' => $this->studentId,
  245. 'kp_code' => $kpCode,
  246. 'error' => $e->getMessage()
  247. ]);
  248. $this->dispatch('notify', message: '操作失败:' . $e->getMessage(), type: 'danger');
  249. }
  250. }
  251. public function batchUpdateSkills(): void
  252. {
  253. try {
  254. $service = new LearningAnalyticsService();
  255. $result = $service->batchUpdateSkillProficiency($this->studentId);
  256. if ($result) {
  257. $this->dispatch('notify', message: '技能熟练度更新完成', type: 'success');
  258. $this->loadDashboardData(); // 刷新数据
  259. } else {
  260. $this->dispatch('notify', message: '技能熟练度更新失败', type: 'danger');
  261. }
  262. } catch (\Exception $e) {
  263. Log::error('批量更新技能熟练度失败', [
  264. 'student_id' => $this->studentId,
  265. 'error' => $e->getMessage()
  266. ]);
  267. $this->dispatch('notify', message: '操作失败:' . $e->getMessage(), type: 'danger');
  268. }
  269. }
  270. public function generateQuickPrediction(): void
  271. {
  272. try {
  273. $service = new LearningAnalyticsService();
  274. $result = $service->quickScorePrediction($this->studentId);
  275. if ($result) {
  276. $this->dispatch('notify', message: '快速预测生成完成', type: 'success');
  277. $this->loadDashboardData(); // 刷新数据
  278. } else {
  279. $this->dispatch('notify', message: '快速预测生成失败', type: 'danger');
  280. }
  281. } catch (\Exception $e) {
  282. Log::error('生成快速预测失败', [
  283. 'student_id' => $this->studentId,
  284. 'error' => $e->getMessage()
  285. ]);
  286. $this->dispatch('notify', message: '操作失败:' . $e->getMessage(), type: 'danger');
  287. }
  288. }
  289. /**
  290. * 从题库API获取题目
  291. */
  292. private function fetchQuestionFromBank(): ?array
  293. {
  294. try {
  295. $questionBankApiBase = config('services.question_bank_api.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015'));
  296. // 调用题库API获取题目
  297. $response = Http::timeout(10)
  298. ->get($questionBankApiBase . '/questions', [
  299. 'limit' => 1,
  300. 'type' => 'factorization'
  301. ]);
  302. if ($response->successful()) {
  303. $data = $response->json();
  304. $questions = $data['data'] ?? [];
  305. if (!empty($questions)) {
  306. $question = $questions[0];
  307. $kpCode = $question['kp_code'] ?? null;
  308. $knowledgePointId = $question['knowledge_point_id'] ?? null;
  309. if ($kpCode && !$knowledgePointId) {
  310. $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
  311. }
  312. if (!$kpCode && $knowledgePointId) {
  313. $kpCode = $this->findKnowledgePointCodeById((string) $knowledgePointId);
  314. }
  315. if (!$knowledgePointId && $this->selectedKnowledgePoint) {
  316. $knowledgePointId = (string) $this->selectedKnowledgePoint;
  317. }
  318. if (!$kpCode && $this->selectedKnowledgePoint) {
  319. $kpCode = $this->findKnowledgePointCodeById((string) $this->selectedKnowledgePoint);
  320. }
  321. if (!$kpCode) {
  322. $kpCode = $this->getDefaultKnowledgePointMeta()['code'];
  323. }
  324. if (!$knowledgePointId && $kpCode) {
  325. $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
  326. }
  327. return [
  328. 'id' => $question['question_code'] ?? 'Q_' . time(),
  329. 'content' => $question['stem'] ?? '',
  330. 'answer' => $question['solution'] ?? '',
  331. 'type' => '因式分解',
  332. 'difficulty' => $question['difficulty'] ?? rand(1, 5),
  333. 'kp_code' => $kpCode ?? 'KP_UNKNOWN',
  334. 'knowledge_point_id' => $knowledgePointId,
  335. 'skill' => $question['skill'] ?? 'unknown'
  336. ];
  337. }
  338. }
  339. // 如果API失败,返回模拟题目
  340. return $this->generateMockQuestion();
  341. } catch (\Exception $e) {
  342. Log::warning('题库API调用失败,使用模拟题目', ['error' => $e->getMessage()]);
  343. return $this->generateMockQuestion();
  344. }
  345. }
  346. /**
  347. * 生成模拟题目(备用方案)
  348. */
  349. private function generateMockQuestion(): array
  350. {
  351. // 如果用户选择了知识点,使用选中的知识点的kp_code
  352. if ($this->selectedKnowledgePoint) {
  353. $selectedKpCode = null;
  354. foreach ($this->availableKnowledgePoints as $kp) {
  355. if ($kp['id'] === $this->selectedKnowledgePoint) {
  356. $selectedKpCode = $kp['code'];
  357. break;
  358. }
  359. }
  360. // 如果找到了选中的知识点的code,使用它
  361. if ($selectedKpCode) {
  362. $mockQuestions = [
  363. ['content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2],
  364. ['content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3],
  365. ['content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2],
  366. ['content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2],
  367. ['content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3],
  368. ];
  369. $question = $mockQuestions[array_rand($mockQuestions)];
  370. $question['type'] = '因式分解';
  371. $question['kp_code'] = $selectedKpCode;
  372. $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint;
  373. return $question;
  374. }
  375. }
  376. // 如果没有选择知识点,使用默认的
  377. $types = [
  378. ['type' => '因式分解', 'kp_code' => 'KP7001', 'content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2],
  379. ['type' => '因式分解', 'kp_code' => 'KP8001', 'content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3],
  380. ['type' => '因式分解', 'kp_code' => 'KP8002', 'content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2],
  381. ['type' => '因式分解', 'kp_code' => 'KP8003', 'content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2],
  382. ['type' => '因式分解', 'kp_code' => 'KP8004', 'content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3],
  383. ];
  384. $question = $types[array_rand($types)];
  385. $kpMeta = $this->getDefaultKnowledgePointMeta();
  386. $question['kp_code'] = $question['kp_code'] ?? 'KP_UNKNOWN';
  387. $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']) ?? $kpMeta['id'];
  388. return $question;
  389. }
  390. /**
  391. * 从题库API获取多道题目
  392. */
  393. private function fetchMultipleQuestionsFromBank(int $count): array
  394. {
  395. try {
  396. $questionBankApiBase = config('services.question_bank_api.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015'));
  397. // 调用题库API一次性获取所有题目
  398. // 如果用户选择了知识点,传入知识点参数
  399. $params = [
  400. 'limit' => $count,
  401. 'type' => 'factorization'
  402. ];
  403. // 如果用户选择了知识点,传入kp_code
  404. if ($this->selectedKnowledgePoint) {
  405. foreach ($this->availableKnowledgePoints as $kp) {
  406. if ($kp['id'] === $this->selectedKnowledgePoint) {
  407. $params['kp_code'] = $kp['code'];
  408. break;
  409. }
  410. }
  411. }
  412. $response = Http::timeout(10)
  413. ->get($questionBankApiBase . '/questions', $params);
  414. if ($response->successful()) {
  415. $data = $response->json();
  416. $questions = $data['data'] ?? [];
  417. $processedQuestions = [];
  418. foreach ($questions as $question) {
  419. $kpCode = $question['kp_code'] ?? null;
  420. $knowledgePointId = $question['knowledge_point_id'] ?? null;
  421. // 如果用户选择了知识点,优先使用选中的知识点
  422. if ($this->selectedKnowledgePoint) {
  423. // 查找选中的知识点的kp_code
  424. $selectedKpCode = null;
  425. foreach ($this->availableKnowledgePoints as $kp) {
  426. if ($kp['id'] === $this->selectedKnowledgePoint) {
  427. $selectedKpCode = $kp['code'];
  428. break;
  429. }
  430. }
  431. if ($selectedKpCode) {
  432. $kpCode = $selectedKpCode;
  433. $knowledgePointId = (string) $this->selectedKnowledgePoint;
  434. }
  435. } else {
  436. // 如果没有选择知识点,使用API返回的或查找
  437. if ($kpCode && !$knowledgePointId) {
  438. $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
  439. }
  440. if (!$kpCode && $knowledgePointId) {
  441. $kpCode = $this->findKnowledgePointCodeById((string) $knowledgePointId);
  442. }
  443. if (!$kpCode) {
  444. $kpCode = $this->getDefaultKnowledgePointMeta()['code'];
  445. }
  446. if (!$knowledgePointId && $kpCode) {
  447. $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
  448. }
  449. }
  450. $processedQuestions[] = [
  451. 'id' => $question['question_code'] ?? 'Q_' . time() . '_' . uniqid(),
  452. 'content' => $question['stem'] ?? '',
  453. 'answer' => $question['solution'] ?? '',
  454. 'type' => '因式分解',
  455. 'difficulty' => $question['difficulty'] ?? rand(1, 5),
  456. 'kp_code' => $kpCode ?? 'KP_UNKNOWN',
  457. 'knowledge_point_id' => $knowledgePointId,
  458. 'skill' => $question['skill'] ?? 'unknown'
  459. ];
  460. // 达到指定数量就停止处理
  461. if (count($processedQuestions) >= $count) {
  462. break;
  463. }
  464. }
  465. // 如果API返回的题目不足,补充模拟题目
  466. while (count($processedQuestions) < $count) {
  467. $mockQuestion = $this->generateMockQuestion();
  468. // 重新生成唯一ID
  469. $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
  470. $processedQuestions[] = $mockQuestion;
  471. }
  472. return $processedQuestions;
  473. }
  474. // 如果API失败,返回模拟题目
  475. $mockQuestions = [];
  476. for ($i = 0; $i < $count; $i++) {
  477. $mockQuestion = $this->generateMockQuestion();
  478. $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
  479. $mockQuestions[] = $mockQuestion;
  480. }
  481. return $mockQuestions;
  482. } catch (\Exception $e) {
  483. Log::warning('题库API调用失败,使用模拟题目', ['error' => $e->getMessage()]);
  484. $mockQuestions = [];
  485. for ($i = 0; $i < $count; $i++) {
  486. $mockQuestion = $this->generateMockQuestion();
  487. $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
  488. $mockQuestions[] = $mockQuestion;
  489. }
  490. return $mockQuestions;
  491. }
  492. }
  493. /**
  494. * 根据答题结果更新掌握度(通过LearningAnalytics API)
  495. */
  496. private function updateMasteryFromAnswer(array $question, bool $isCorrect): void
  497. {
  498. try {
  499. $learningAnalytics = new LearningAnalyticsService();
  500. $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($question['knowledge_point_id'] ?? null);
  501. $attemptData = [
  502. 'kp_code' => $kpCode ?? 'KP_UNKNOWN',
  503. 'is_correct' => $isCorrect,
  504. 'time_spent_seconds' => rand(60, 180),
  505. 'difficulty_level' => (string)($question['difficulty'] ?? '3'),
  506. 'question_id' => 'Q_' . time(),
  507. 'student_answer' => $this->userAnswer ?: '',
  508. 'correct_answer' => $question['answer'] ?? '',
  509. ];
  510. if (!empty($question['knowledge_point_id'])) {
  511. $attemptData['knowledge_point_id'] = $question['knowledge_point_id'];
  512. }
  513. // 添加技能点数据(从题目中或当前选择的技能中获取)
  514. if (!empty($question['selected_skills'])) {
  515. $attemptData['skill_codes'] = $question['selected_skills'];
  516. } elseif (!empty($this->selectedSkills)) {
  517. $attemptData['skill_codes'] = $this->selectedSkills;
  518. } else {
  519. $attemptData['skill_codes'] = [];
  520. }
  521. $result = $learningAnalytics->submitAttempt($this->studentId, $attemptData);
  522. if (isset($result['error'])) {
  523. Log::error('LearningAnalytics API 调用失败', [
  524. 'student_id' => $this->studentId,
  525. 'error' => $result['message'] ?? 'Unknown error',
  526. 'attempt_data' => $attemptData
  527. ]);
  528. } else {
  529. Log::info('答题记录已成功提交到 LearningAnalytics', [
  530. 'student_id' => $this->studentId,
  531. 'attempt_id' => $result['attempt_id'] ?? null,
  532. 'mastery_level' => $result['mastery_level'] ?? null,
  533. 'knowledge_point_id' => $result['knowledge_point_id'] ?? null,
  534. 'skill_codes' => $result['skill_codes'] ?? []
  535. ]);
  536. }
  537. } catch (\Exception $e) {
  538. Log::error('更新掌握度失败', [
  539. 'student_id' => $this->studentId,
  540. 'error' => $e->getMessage(),
  541. 'trace' => $e->getTraceAsString()
  542. ]);
  543. // 不再抛出异常,避免影响用户体验
  544. }
  545. }
  546. /**
  547. * 加载知识点和技能数据
  548. */
  549. public function loadKnowledgePointsAndSkills(): void
  550. {
  551. try {
  552. $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
  553. // 从知识图谱API获取知识点数据
  554. $kpResponse = Http::timeout(10)
  555. ->get($knowledgeApiBase . '/knowledge-points/', [
  556. 'page' => 1,
  557. 'per_page' => 100
  558. ]);
  559. if ($kpResponse->successful()) {
  560. $kpData = $kpResponse->json();
  561. $this->availableKnowledgePoints = $kpData['data'] ?? $kpData ?? [];
  562. // 格式化知识点数据,确保包含必要的字段
  563. $this->availableKnowledgePoints = array_map(function($kp) {
  564. return [
  565. 'id' => (string)($kp['id'] ?? $kp['kp_id'] ?? uniqid()),
  566. 'code' => $kp['kp_code'] ?? $kp['kp_id'] ?? $kp['code'] ?? 'KP_UNKNOWN',
  567. 'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kp['name'] ?? $kp['kp_code'] ?? '未知知识点',
  568. 'subject' => $kp['category'] ?? '数学'
  569. ];
  570. }, $this->availableKnowledgePoints);
  571. } else {
  572. throw new \Exception('知识图谱API调用失败: ' . $kpResponse->status());
  573. }
  574. // 从知识图谱API获取技能数据
  575. $skillResponse = Http::timeout(10)
  576. ->get($knowledgeApiBase . '/skills/', [
  577. 'page' => 1,
  578. 'per_page' => 50
  579. ]);
  580. if ($skillResponse->successful()) {
  581. $skillData = $skillResponse->json();
  582. $this->availableSkills = $skillData['data'] ?? $skillData ?? [];
  583. // 格式化技能数据
  584. $this->availableSkills = array_map(function($skill) {
  585. return [
  586. 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
  587. 'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
  588. 'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
  589. 'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
  590. ];
  591. }, $this->availableSkills);
  592. } else {
  593. throw new \Exception('技能API调用失败: ' . $skillResponse->status());
  594. }
  595. Log::info('成功从知识图谱API加载数据', [
  596. 'knowledge_points_count' => count($this->availableKnowledgePoints),
  597. 'skills_count' => count($this->availableSkills)
  598. ]);
  599. } catch (\Exception $e) {
  600. Log::error('从知识图谱API加载知识点和技能数据失败,使用备用数据', [
  601. 'error' => $e->getMessage(),
  602. 'trace' => $e->getTraceAsString()
  603. ]);
  604. // 使用模拟数据作为备用
  605. $this->availableKnowledgePoints = [
  606. ['id' => 'factor_1', 'code' => 'factor_1', 'name' => '因式分解基础', 'subject' => '数学'],
  607. ['id' => 'factor_2', 'code' => 'factor_2', 'name' => '提取公因式', 'subject' => '数学'],
  608. ['id' => 'factor_3', 'code' => 'factor_3', 'name' => '平方差公式', 'subject' => '数学'],
  609. ['id' => 'factor_4', 'code' => 'factor_4', 'name' => '完全平方公式', 'subject' => '数学'],
  610. ['id' => 'factor_5', 'code' => 'factor_5', 'name' => '分组分解法', 'subject' => '数学'],
  611. ['id' => 'factor_6', 'code' => 'factor_6', 'name' => '立方和差公式', 'subject' => '数学'],
  612. ['id' => 'factor_7', 'code' => 'factor_7', 'name' => '十字相乘法', 'subject' => '数学'],
  613. ['id' => 'factor_8', 'code' => 'factor_8', 'name' => '综合因式分解', 'subject' => '数学'],
  614. ];
  615. $this->availableSkills = [
  616. ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'],
  617. ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'],
  618. ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'],
  619. ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'],
  620. ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'],
  621. ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'],
  622. ];
  623. }
  624. $this->buildKnowledgePointIndexes();
  625. // 不在这里初始化联动,让用户手动选择后再加载
  626. // 避免在页面加载时就调用API
  627. Log::info('知识点和技能数据加载完成');
  628. }
  629. /**
  630. * 为知识点构建索引映射
  631. */
  632. private function buildKnowledgePointIndexes(): void
  633. {
  634. $this->knowledgePointCodeIndex = [];
  635. $this->knowledgePointIdIndex = [];
  636. foreach ($this->availableKnowledgePoints as $kp) {
  637. // 使用格式化后的字段:code 对应 kp_code
  638. if (!empty($kp['code'])) {
  639. $this->knowledgePointCodeIndex[(string) $kp['code']] = $kp;
  640. }
  641. // 使用格式化后的字段:id
  642. if (!empty($kp['id'])) {
  643. $this->knowledgePointIdIndex[(string) $kp['id']] = $kp;
  644. }
  645. }
  646. }
  647. /**
  648. * 将技能ID数组转换为技能名称数组
  649. */
  650. private function convertSkillIdsToNames(array $skillIds): array
  651. {
  652. $skillNames = [];
  653. foreach ($skillIds as $skillId) {
  654. foreach ($this->availableSkills as $skill) {
  655. if ((string)$skill['id'] === (string)$skillId || (string)$skill['code'] === (string)$skillId) {
  656. $skillNames[] = $skill['name'];
  657. break;
  658. }
  659. }
  660. }
  661. return $skillNames;
  662. }
  663. /**
  664. * 通过 ID 或 code 查找知识点
  665. */
  666. private function findKnowledgePointByIdOrCode(?string $identifier): ?array
  667. {
  668. if (empty($identifier)) {
  669. return null;
  670. }
  671. if (isset($this->knowledgePointIdIndex[$identifier])) {
  672. return $this->knowledgePointIdIndex[$identifier];
  673. }
  674. if (isset($this->knowledgePointCodeIndex[$identifier])) {
  675. return $this->knowledgePointCodeIndex[$identifier];
  676. }
  677. return $this->findKnowledgePointByCode($identifier);
  678. }
  679. /**
  680. * 通过知识点 code 获取详情
  681. */
  682. private function findKnowledgePointByCode(?string $kpCode): ?array
  683. {
  684. if (empty($kpCode)) {
  685. return null;
  686. }
  687. if (isset($this->knowledgePointCodeIndex[$kpCode])) {
  688. return $this->knowledgePointCodeIndex[$kpCode];
  689. }
  690. $fetched = $this->fetchKnowledgePointFromApi($kpCode);
  691. if ($fetched) {
  692. $this->availableKnowledgePoints[] = $fetched;
  693. $this->buildKnowledgePointIndexes();
  694. }
  695. return $fetched;
  696. }
  697. /**
  698. * 调用知识图谱 API 获取知识点
  699. */
  700. private function fetchKnowledgePointFromApi(string $kpCode): ?array
  701. {
  702. try {
  703. $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
  704. $response = Http::timeout(10)->get($knowledgeApiBase . '/knowledge-points/' . $kpCode);
  705. if ($response->successful()) {
  706. $kp = $response->json();
  707. return [
  708. 'id' => (string) ($kp['id'] ?? $kp['kp_id'] ?? $kpCode),
  709. 'code' => $kp['kp_code'] ?? $kpCode,
  710. 'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kpCode,
  711. 'subject' => $kp['category'] ?? '数学'
  712. ];
  713. }
  714. } catch (\Exception $e) {
  715. Log::warning('获取知识点详情失败', [
  716. 'kp_code' => $kpCode,
  717. 'error' => $e->getMessage()
  718. ]);
  719. }
  720. return null;
  721. }
  722. /**
  723. * 获取知识点 ID(根据 code)
  724. */
  725. private function findKnowledgePointIdByCode(?string $kpCode): ?string
  726. {
  727. $kp = $this->findKnowledgePointByCode($kpCode);
  728. if (!$kp || empty($kp['id'])) {
  729. return null;
  730. }
  731. return (string) $kp['id'];
  732. }
  733. /**
  734. * 根据 ID 获取知识点 code
  735. */
  736. private function findKnowledgePointCodeById(?string $kpId): ?string
  737. {
  738. $kp = $this->findKnowledgePointByIdOrCode($kpId);
  739. return $kp['code'] ?? null;
  740. }
  741. /**
  742. * 获取一个默认的知识点(用于兜底数据)
  743. */
  744. private function getDefaultKnowledgePointMeta(): array
  745. {
  746. if (!empty($this->availableKnowledgePoints)) {
  747. $kp = $this->availableKnowledgePoints[array_rand($this->availableKnowledgePoints)];
  748. return [
  749. 'id' => isset($kp['id']) ? (string) $kp['id'] : null,
  750. 'code' => $kp['code'] ?? null
  751. ];
  752. }
  753. return [
  754. 'id' => null,
  755. 'code' => 'KP_UNKNOWN'
  756. ];
  757. }
  758. /**
  759. * 知识点选择变化时更新技能列表
  760. */
  761. public function updatedSelectedKnowledgePoint(): void
  762. {
  763. // 清空已选择的技能
  764. $this->selectedSkills = [];
  765. // 如果没有选择知识点,加载所有技能
  766. if (empty($this->selectedKnowledgePoint)) {
  767. $this->loadAllSkills();
  768. return;
  769. }
  770. // 根据选择的知识点获取相关技能
  771. $this->loadSkillsForKnowledgePoint($this->selectedKnowledgePoint);
  772. }
  773. /**
  774. * 根据知识点加载相关技能
  775. */
  776. private function loadSkillsForKnowledgePoint(string $knowledgePointId): void
  777. {
  778. $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
  779. // 根据knowledgePointId查找对应的kp_code
  780. $kpCode = null;
  781. foreach ($this->availableKnowledgePoints as $kp) {
  782. if ($kp['id'] === $knowledgePointId) {
  783. $kpCode = $kp['code']; // 使用kp_code作为API参数
  784. break;
  785. }
  786. }
  787. if (!$kpCode) {
  788. Log::warning('未找到知识点对应的kp_code', ['knowledge_point_id' => $knowledgePointId]);
  789. $this->availableSkills = [];
  790. return;
  791. }
  792. Log::info('准备调用知识点详情API', [
  793. 'kp_code' => $kpCode,
  794. 'knowledge_point_id' => $knowledgePointId
  795. ]);
  796. // 直接从知识点详情API获取技能列表
  797. $kpDetailResponse = Http::timeout(10)
  798. ->get($knowledgeApiBase . '/knowledge-points/' . $kpCode);
  799. $kpData = $kpDetailResponse->json();
  800. // 打印完整响应,方便调试
  801. Log::info('知识点API完整响应', [
  802. 'knowledge_point' => $kpCode,
  803. 'status' => $kpDetailResponse->status(),
  804. 'response' => $kpData
  805. ]);
  806. // 转换技能数据格式,匹配模板期望的字段名
  807. $skills = $kpData['skills'] ?? [];
  808. $this->availableSkills = array_map(function($skill) {
  809. return [
  810. 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? ''),
  811. 'code' => $skill['skill_code'] ?? '',
  812. 'name' => $skill['skill_name'] ?? '',
  813. 'category' => $skill['skill_type'] ?? ''
  814. ];
  815. }, $skills);
  816. Log::info('设置技能列表', [
  817. 'count' => count($this->availableSkills),
  818. 'skills' => $this->availableSkills
  819. ]);
  820. }
  821. /**
  822. * 加载所有技能
  823. */
  824. private function loadAllSkills(): void
  825. {
  826. // 先保存当前技能列表作为兜底
  827. $fallbackSkills = $this->availableSkills;
  828. try {
  829. $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
  830. $skillResponse = Http::timeout(10)
  831. ->get($knowledgeApiBase . '/skills/', [
  832. 'page' => 1,
  833. 'per_page' => 50
  834. ]);
  835. if ($skillResponse->successful()) {
  836. $skillData = $skillResponse->json();
  837. $skills = $skillData['data'] ?? $skillData ?? [];
  838. // 只有当API返回有效数据时才更新技能列表
  839. if (!empty($skills) && is_array($skills)) {
  840. $this->availableSkills = array_map(function($skill) {
  841. return [
  842. 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
  843. 'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
  844. 'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
  845. 'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
  846. ];
  847. }, $skills);
  848. Log::info('成功加载所有技能', [
  849. 'skills_count' => count($this->availableSkills)
  850. ]);
  851. return;
  852. }
  853. }
  854. // 如果API调用失败或返回空数据,使用默认技能列表
  855. Log::warning('加载所有技能失败或为空,使用默认技能列表');
  856. $this->useDefaultSkills();
  857. } catch (\Exception $e) {
  858. Log::error('加载所有技能失败,使用默认技能列表', ['error' => $e->getMessage()]);
  859. $this->useDefaultSkills();
  860. }
  861. }
  862. /**
  863. * 使用默认技能列表
  864. */
  865. private function useDefaultSkills(): void
  866. {
  867. $this->availableSkills = [
  868. ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'],
  869. ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'],
  870. ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'],
  871. ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'],
  872. ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'],
  873. ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'],
  874. ];
  875. }
  876. /**
  877. * 生成批量题目
  878. */
  879. public function generateBatchQuestions(): void
  880. {
  881. if (empty($this->studentId)) {
  882. $this->dispatch('notify', message: '请先选择学生', type: 'warning');
  883. return;
  884. }
  885. try {
  886. $this->isLoading = true;
  887. // 生成批次ID
  888. $this->currentBatchId = 'BATCH_' . $this->studentId . '_' . time();
  889. // 一次性获取所有需要的题目,避免重复调用API
  890. $allQuestions = $this->fetchMultipleQuestionsFromBank($this->questionsPerSet);
  891. // 处理题目
  892. $questions = [];
  893. foreach ($allQuestions as $question) {
  894. if (empty($question['knowledge_point_id']) && !empty($question['kp_code'])) {
  895. $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']);
  896. }
  897. if (empty($question['knowledge_point_id']) && $this->selectedKnowledgePoint) {
  898. $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint;
  899. }
  900. // 添加选择的知识点和技能信息
  901. $question['batch_id'] = $this->currentBatchId;
  902. $question['selected_knowledge_point'] = $this->selectedKnowledgePoint;
  903. $question['selected_skills'] = $this->selectedSkills;
  904. $questions[] = $question;
  905. }
  906. if (!empty($questions)) {
  907. $this->exerciseQuestions = $questions;
  908. // 初始化答题数组
  909. $this->exerciseAnswers = [];
  910. foreach ($questions as $index => $question) {
  911. $this->exerciseAnswers[$index] = [
  912. 'user_answer' => '',
  913. 'is_correct' => null,
  914. ];
  915. }
  916. $this->dispatch('notify', message: "成功生成 {$this->questionsPerSet} 道题目", type: 'success');
  917. } else {
  918. $this->dispatch('notify', message: '生成题目失败,请重试', type: 'danger');
  919. }
  920. } catch (\Exception $e) {
  921. Log::error('生成批量题目失败', [
  922. 'student_id' => $this->studentId,
  923. 'questions_count' => $this->questionsPerSet,
  924. 'error' => $e->getMessage()
  925. ]);
  926. $this->dispatch('notify', message: '生成题目失败:' . $e->getMessage(), type: 'danger');
  927. } finally {
  928. $this->isLoading = false;
  929. }
  930. }
  931. /**
  932. * 批量提交答案
  933. */
  934. public function submitBatchAnswers(): void
  935. {
  936. if (empty($this->studentId) || empty($this->exerciseQuestions) || empty($this->currentBatchId)) {
  937. $this->dispatch('notify', message: '没有可提交的题目', type: 'warning');
  938. return;
  939. }
  940. try {
  941. $this->isLoading = true;
  942. $successCount = 0;
  943. $failureCount = 0;
  944. foreach ($this->exerciseQuestions as $index => $question) {
  945. $answer = $this->exerciseAnswers[$index] ?? null;
  946. if (!$answer || $answer['is_correct'] === null) {
  947. continue; // 跳过未答题的题目
  948. }
  949. try {
  950. // 确保knowledge_point_id始终是数字ID
  951. $knowledgePointId = $question['knowledge_point_id'] ?? null;
  952. // 如果knowledge_point_id是code,转换为ID
  953. if ($knowledgePointId && !is_numeric($knowledgePointId)) {
  954. $knowledgePointId = $this->findKnowledgePointIdByCode($knowledgePointId);
  955. }
  956. // 如果还是没有,使用选中的知识点
  957. if (!$knowledgePointId && $this->selectedKnowledgePoint) {
  958. $selectedValue = $this->selectedKnowledgePoint;
  959. // 如果选中的是code,转换为ID;如果是ID,直接使用
  960. if (!is_numeric($selectedValue)) {
  961. $knowledgePointId = $this->findKnowledgePointIdByCode($selectedValue);
  962. } else {
  963. $knowledgePointId = (string)$selectedValue;
  964. }
  965. }
  966. // 如果还是没有,从题目kp_code查找
  967. if (!$knowledgePointId && !empty($question['kp_code'])) {
  968. $knowledgePointId = $this->findKnowledgePointIdByCode($question['kp_code']);
  969. }
  970. $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($knowledgePointId) ?? 'KP_UNKNOWN';
  971. // 准备数据库存储数据
  972. $exerciseData = [
  973. 'student_id' => $this->studentId,
  974. 'question_id' => $question['id'] ?? 'Q_' . $this->currentBatchId . '_' . $index,
  975. // 确保knowledge_point_id是整数或null,不能是字符串
  976. 'knowledge_point_id' => is_numeric($knowledgePointId) ? (int)$knowledgePointId : null,
  977. 'question_content' => $question['content'] ?? '',
  978. 'student_answer' => $answer['user_answer'] ?? '',
  979. 'correct_answer' => $question['answer'] ?? '',
  980. 'is_correct' => $answer['is_correct'],
  981. 'submission_status' => 'submitted',
  982. 'batch_id' => $this->currentBatchId,
  983. 'kp_code' => $kpCode,
  984. 'selected_skills' => json_encode($this->selectedSkills),
  985. 'skill_scores' => $this->calculateSkillScores($this->selectedSkills, $answer['is_correct']),
  986. 'time_spent_seconds' => rand(60, 180),
  987. 'difficulty_level' => is_numeric($question['difficulty'] ?? 3) ? (float)$question['difficulty'] : 3,
  988. 'created_at' => now(),
  989. 'updated_at' => now(),
  990. ];
  991. // 保存到 Laravel 数据库
  992. \App\Models\StudentExercise::create($exerciseData);
  993. // 提交给 LearningAnalytics 系统
  994. $this->updateMasteryFromBatchAnswer($question, $answer['is_correct']);
  995. $successCount++;
  996. } catch (\Exception $e) {
  997. Log::error('批量答题中的单题提交失败', [
  998. 'student_id' => $this->studentId,
  999. 'question_index' => $index,
  1000. 'error' => $e->getMessage()
  1001. ]);
  1002. $failureCount++;
  1003. }
  1004. }
  1005. // 清空批量数据
  1006. $this->exerciseQuestions = [];
  1007. $this->exerciseAnswers = [];
  1008. $this->currentBatchId = '';
  1009. // 提交结果
  1010. $totalQuestions = $successCount + $failureCount;
  1011. $this->dispatch('notify',
  1012. message: "批量提交完成!成功: {$successCount} 题,失败: {$failureCount} 题",
  1013. type: $failureCount === 0 ? 'success' : 'warning'
  1014. );
  1015. // 刷新仪表板数据
  1016. $this->loadDashboardData();
  1017. // 批量更新技能熟练度
  1018. try {
  1019. $learningAnalytics = new LearningAnalyticsService();
  1020. $skillResult = $learningAnalytics->batchUpdateSkillProficiency($this->studentId);
  1021. if ($skillResult) {
  1022. Log::info('技能熟练度批量更新成功', ['student_id' => $this->studentId]);
  1023. }
  1024. } catch (\Exception $e) {
  1025. Log::warning('技能熟练度批量更新失败(不影响答题提交)', [
  1026. 'student_id' => $this->studentId,
  1027. 'error' => $e->getMessage()
  1028. ]);
  1029. }
  1030. } catch (\Exception $e) {
  1031. Log::error('批量提交答案失败', [
  1032. 'student_id' => $this->studentId,
  1033. 'batch_id' => $this->currentBatchId,
  1034. 'error' => $e->getMessage()
  1035. ]);
  1036. $this->dispatch('notify', message: '批量提交失败:' . $e->getMessage(), type: 'danger');
  1037. } finally {
  1038. $this->isLoading = false;
  1039. }
  1040. }
  1041. /**
  1042. * 计算技能评分
  1043. */
  1044. private function calculateSkillScores(array $selectedSkills, bool $isCorrect): string
  1045. {
  1046. if (empty($selectedSkills)) {
  1047. return json_encode([]);
  1048. }
  1049. $scores = [];
  1050. $baseScore = $isCorrect ? 0.8 : 0.2; // 正确答对给0.8分,错误给0.2分
  1051. $randomFactor = rand(-10, 10) / 100; // 添加随机因素
  1052. foreach ($selectedSkills as $skillId) {
  1053. $score = max(0, min(1, $baseScore + $randomFactor));
  1054. $scores[$skillId] = round($score, 3);
  1055. }
  1056. return json_encode($scores);
  1057. }
  1058. /**
  1059. * 批量答题时更新掌握度
  1060. */
  1061. private function updateMasteryFromBatchAnswer(array $question, bool $isCorrect): void
  1062. {
  1063. try {
  1064. $learningAnalytics = new LearningAnalyticsService();
  1065. $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($question['knowledge_point_id'] ?? null);
  1066. $attemptData = [
  1067. 'kp_code' => $kpCode ?? 'KP_UNKNOWN',
  1068. 'is_correct' => $isCorrect,
  1069. 'time_spent_seconds' => rand(60, 180),
  1070. 'difficulty_level' => (string)($question['difficulty'] ?? '3'),
  1071. 'question_id' => 'Q_' . $this->currentBatchId . '_' . rand(1000, 9999),
  1072. 'student_answer' => '',
  1073. 'correct_answer' => $question['answer'] ?? '',
  1074. ];
  1075. if (!empty($question['knowledge_point_id'])) {
  1076. $attemptData['knowledge_point_id'] = $question['knowledge_point_id'];
  1077. }
  1078. // 添加技能点数据(使用技能ID,ID是唯一的)
  1079. if (!empty($question['selected_skills'])) {
  1080. $attemptData['skill_codes'] = $question['selected_skills'];
  1081. } elseif (!empty($this->selectedSkills)) {
  1082. $attemptData['skill_codes'] = $this->selectedSkills;
  1083. } else {
  1084. $attemptData['skill_codes'] = [];
  1085. }
  1086. $result = $learningAnalytics->submitAttempt($this->studentId, $attemptData);
  1087. if (isset($result['error'])) {
  1088. Log::error('LearningAnalytics API 调用失败', [
  1089. 'student_id' => $this->studentId,
  1090. 'batch_id' => $this->currentBatchId,
  1091. 'error' => $result['message'] ?? 'Unknown error',
  1092. 'attempt_data' => $attemptData
  1093. ]);
  1094. } else {
  1095. Log::info('批量答题记录已成功提交', [
  1096. 'student_id' => $this->studentId,
  1097. 'batch_id' => $this->currentBatchId,
  1098. 'attempt_id' => $result['attempt_id'] ?? null,
  1099. 'knowledge_point_id' => $result['knowledge_point_id'] ?? null,
  1100. 'skill_codes' => $result['skill_codes'] ?? []
  1101. ]);
  1102. }
  1103. } catch (\Exception $e) {
  1104. Log::error('更新批量答题掌握度失败', [
  1105. 'student_id' => $this->studentId,
  1106. 'batch_id' => $this->currentBatchId,
  1107. 'error' => $e->getMessage()
  1108. ]);
  1109. }
  1110. }
  1111. /**
  1112. * 清除历史记录
  1113. */
  1114. public function clearHistory(): void
  1115. {
  1116. $this->questionHistory = [];
  1117. $this->dispatch('notify', message: '历史记录已清除', type: 'info');
  1118. }
  1119. /**
  1120. * 清空学生的所有答题数据
  1121. */
  1122. public function clearStudentAllData(): void
  1123. {
  1124. if (empty($this->studentId)) {
  1125. $this->dispatch('notify', message: '请先选择学生', type: 'warning');
  1126. return;
  1127. }
  1128. try {
  1129. $this->isLoading = true;
  1130. $service = new LearningAnalyticsService();
  1131. $result = $service->clearStudentData($this->studentId);
  1132. if ($result) {
  1133. $this->dispatch('notify', message: '学生答题数据已清空', type: 'success');
  1134. // 清空当前仪表板数据
  1135. $this->dashboardData = [];
  1136. // 重新加载仪表板数据
  1137. $this->loadDashboardData();
  1138. } else {
  1139. $this->dispatch('notify', message: '清空数据时发生错误,请检查日志', type: 'danger');
  1140. }
  1141. } catch (\Exception $e) {
  1142. Log::error('清空学生数据失败', [
  1143. 'student_id' => $this->studentId,
  1144. 'error' => $e->getMessage()
  1145. ]);
  1146. $this->dispatch('notify', message: '清空数据失败:' . $e->getMessage(), type: 'danger');
  1147. } finally {
  1148. $this->isLoading = false;
  1149. }
  1150. }
  1151. }