LearningAnalyticsService.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Http;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Support\Facades\DB;
  6. class LearningAnalyticsService
  7. {
  8. protected string $baseUrl;
  9. protected int $timeout = 10;
  10. protected ?QuestionBankService $questionBankService;
  11. public function __construct(?QuestionBankService $questionBankService = null)
  12. {
  13. $this->baseUrl = config('services.learning_analytics.url', env('LEARNING_ANALYTICS_API_BASE', 'http://localhost:5016'));
  14. $this->questionBankService = $questionBankService;
  15. }
  16. /**
  17. * 获取学生掌握度
  18. */
  19. public function getStudentMastery(string $studentId, string $kpCode = null): array
  20. {
  21. try {
  22. $endpoint = $kpCode
  23. ? "/api/v1/mastery/student/{$studentId}/kp/{$kpCode}"
  24. : "/api/v1/mastery/student/{$studentId}";
  25. $response = Http::timeout($this->timeout)->get($this->baseUrl . $endpoint);
  26. if ($response->successful()) {
  27. return $response->json();
  28. }
  29. Log::error('LearningAnalytics API Error', [
  30. 'endpoint' => $endpoint,
  31. 'status' => $response->status(),
  32. 'response' => $response->body()
  33. ]);
  34. return [
  35. 'error' => true,
  36. 'message' => 'Failed to fetch mastery data'
  37. ];
  38. } catch (\Exception $e) {
  39. Log::error('LearningAnalytics Service Exception', [
  40. 'error' => $e->getMessage(),
  41. 'trace' => $e->getTraceAsString()
  42. ]);
  43. return [
  44. 'error' => true,
  45. 'message' => $e->getMessage()
  46. ];
  47. }
  48. }
  49. /**
  50. * 更新学生掌握度
  51. */
  52. public function updateMastery(array $data): array
  53. {
  54. try {
  55. $response = Http::timeout($this->timeout)
  56. ->post($this->baseUrl . '/api/v1/mastery/student/' . $data['student_id'] . '/update', $data);
  57. if ($response->successful()) {
  58. return $response->json();
  59. }
  60. Log::error('LearningAnalytics Update Error', [
  61. 'data' => $data,
  62. 'status' => $response->status(),
  63. 'response' => $response->body()
  64. ]);
  65. return [
  66. 'error' => true,
  67. 'message' => 'Failed to update mastery'
  68. ];
  69. } catch (\Exception $e) {
  70. Log::error('LearningAnalytics Update Exception', [
  71. 'error' => $e->getMessage(),
  72. 'data' => $data
  73. ]);
  74. return [
  75. 'error' => true,
  76. 'message' => $e->getMessage()
  77. ];
  78. }
  79. }
  80. /**
  81. * 获取老师名下的所有学生
  82. */
  83. public function getTeacherStudents(string $teacherId): array
  84. {
  85. try {
  86. // 从本地MySQL获取学生
  87. $students = DB::table('students as s')
  88. ->leftJoin('users as u', 's.student_id', '=', 'u.user_id')
  89. ->where('s.teacher_id', $teacherId)
  90. ->select(
  91. 's.student_id',
  92. 's.name',
  93. 's.grade',
  94. 's.class_name',
  95. 'u.username',
  96. 'u.email'
  97. )
  98. ->get()
  99. ->toArray();
  100. return $students;
  101. } catch (\Exception $e) {
  102. Log::error('Get Teacher Students Error', [
  103. 'teacher_id' => $teacherId,
  104. 'error' => $e->getMessage()
  105. ]);
  106. return [];
  107. }
  108. }
  109. /**
  110. * 获取学生学习分析
  111. */
  112. public function getStudentAnalysis(string $studentId): array
  113. {
  114. // 从LearningAnalytics获取掌握度
  115. $masteryData = $this->getStudentMastery($studentId);
  116. // 从MySQL获取练习历史
  117. $exercises = DB::table('student_exercises')
  118. ->where('student_id', $studentId)
  119. ->orderBy('created_at', 'desc')
  120. ->limit(50)
  121. ->get()
  122. ->toArray();
  123. // 从MySQL获取掌握度记录
  124. $masteryRecords = DB::table('student_mastery')
  125. ->where('student_id', $studentId)
  126. ->get()
  127. ->toArray();
  128. return [
  129. 'student_id' => $studentId,
  130. 'mastery_from_la' => $masteryData,
  131. 'exercises' => $exercises,
  132. 'mastery_records' => $masteryRecords,
  133. 'total_exercises' => count($exercises),
  134. 'total_mastery_records' => count($masteryRecords),
  135. ];
  136. }
  137. /**
  138. * 生成学习测试数据
  139. */
  140. public function generateLearningData(string $studentId, array $params): array
  141. {
  142. $results = [];
  143. foreach ($params as $param) {
  144. $data = [
  145. 'student_id' => $studentId,
  146. 'kp_code' => $param['kp_code'],
  147. 'is_correct' => $param['is_correct'],
  148. 'time_spent_seconds' => $param['time_spent_seconds'] ?? 120,
  149. 'difficulty_level' => $param['difficulty_level'] ?? 3,
  150. ];
  151. $result = $this->updateMastery($data);
  152. $results[] = $result;
  153. }
  154. return $results;
  155. }
  156. /**
  157. * 获取学习推荐
  158. */
  159. public function getLearningRecommendations(string $studentId): array
  160. {
  161. try {
  162. $response = Http::timeout($this->timeout)
  163. ->get($this->baseUrl . "/api/v1/learning-path/student/{$studentId}/recommend");
  164. if ($response->successful()) {
  165. return $response->json();
  166. }
  167. return ['error' => true, 'message' => 'Failed to fetch recommendations'];
  168. } catch (\Exception $e) {
  169. return ['error' => true, 'message' => $e->getMessage()];
  170. }
  171. }
  172. /**
  173. * 获取知识点列表(从知识图谱API)
  174. */
  175. public function getKnowledgePoints(array $filters = []): array
  176. {
  177. try {
  178. $kgBaseUrl = config('services.knowledge_api.base_url', 'http://localhost:5011');
  179. $response = Http::timeout($this->timeout)
  180. ->get($kgBaseUrl . '/knowledge-points/', $filters);
  181. if ($response->successful()) {
  182. return $response->json()['data'] ?? [];
  183. }
  184. return [];
  185. } catch (\Exception $e) {
  186. Log::error('LearningAnalytics Knowledge Points Error', [
  187. 'error' => $e->getMessage()
  188. ]);
  189. return [];
  190. }
  191. }
  192. /**
  193. * 获取学生技能熟练度
  194. */
  195. public function getStudentSkillProficiency(string $studentId): array
  196. {
  197. try {
  198. $response = Http::timeout($this->timeout)
  199. ->get($this->baseUrl . "/api/v1/skill/proficiency/student/{$studentId}");
  200. if ($response->successful()) {
  201. return $response->json();
  202. }
  203. Log::warning('LearningAnalytics Skill Proficiency API Error', [
  204. 'student_id' => $studentId,
  205. 'status' => $response->status(),
  206. 'response' => $response->body()
  207. ]);
  208. // API失败时返回空数据,不报错
  209. return [
  210. 'student_id' => $studentId,
  211. 'total_count' => 0,
  212. 'data' => []
  213. ];
  214. } catch (\Exception $e) {
  215. Log::warning('LearningAnalytics Skill Proficiency API Exception', [
  216. 'student_id' => $studentId,
  217. 'error' => $e->getMessage()
  218. ]);
  219. // 发生异常时返回空数据,不报错
  220. return [
  221. 'student_id' => $studentId,
  222. 'total_count' => 0,
  223. 'data' => []
  224. ];
  225. }
  226. }
  227. /**
  228. * 获取学生掌握度列表(别名方法)
  229. */
  230. public function getStudentMasteryList(string $studentId): array
  231. {
  232. return $this->getStudentMastery($studentId);
  233. }
  234. /**
  235. * 获取知识点依赖关系
  236. */
  237. public function getKnowledgeDependencies(): array
  238. {
  239. try {
  240. $response = Http::timeout($this->timeout)
  241. ->get($this->baseUrl . '/knowledge-dependencies/');
  242. if ($response->successful()) {
  243. return $response->json()['data'] ?? [];
  244. }
  245. return [];
  246. } catch (\Exception $e) {
  247. Log::error('LearningAnalytics Knowledge Dependencies Error', [
  248. 'error' => $e->getMessage()
  249. ]);
  250. return [];
  251. }
  252. }
  253. /**
  254. * 提交学生答题记录
  255. */
  256. public function submitAttempt(string $studentId, array $attemptData): array
  257. {
  258. try {
  259. $response = Http::timeout($this->timeout)
  260. ->post($this->baseUrl . "/api/v1/attempts/student/{$studentId}", $attemptData);
  261. if ($response->successful()) {
  262. return $response->json();
  263. }
  264. Log::error('Submit Attempt Error', [
  265. 'student_id' => $studentId,
  266. 'data' => $attemptData,
  267. 'status' => $response->status(),
  268. 'response' => $response->body()
  269. ]);
  270. return [
  271. 'error' => true,
  272. 'message' => 'Failed to submit attempt'
  273. ];
  274. } catch (\Exception $e) {
  275. Log::error('Submit Attempt Exception', [
  276. 'student_id' => $studentId,
  277. 'error' => $e->getMessage(),
  278. 'data' => $attemptData
  279. ]);
  280. return [
  281. 'error' => true,
  282. 'message' => $e->getMessage()
  283. ];
  284. }
  285. }
  286. /**
  287. * 检查服务健康状态
  288. */
  289. public function checkHealth(): bool
  290. {
  291. try {
  292. $response = Http::timeout(5)->get($this->baseUrl . '/health');
  293. return $response->successful();
  294. } catch (\Exception $e) {
  295. return false;
  296. }
  297. }
  298. /**
  299. * 获取学生掌握度概览
  300. */
  301. public function getStudentMasteryOverview(string $studentId): array
  302. {
  303. try {
  304. $mastery = $this->getStudentMastery($studentId);
  305. if (isset($mastery['error'])) {
  306. return [
  307. 'total_knowledge_points' => 0,
  308. 'average_mastery_level' => 0,
  309. 'mastered_knowledge_points' => 0,
  310. 'good_knowledge_points' => 0,
  311. 'weak_knowledge_points' => 0,
  312. 'weak_knowledge_points_list' => [],
  313. 'details' => []
  314. ];
  315. }
  316. $data = $mastery['data'] ?? [];
  317. // **修复**:不过滤total_attempts,与薄弱点API保持一致
  318. // 这样确保数据一致性
  319. $attemptedData = $data;
  320. $total = count($data);
  321. $attemptedCount = count($attemptedData);
  322. $average = $attemptedCount > 0
  323. ? array_sum(array_column($attemptedData, 'mastery_level')) / $attemptedCount
  324. : 0;
  325. // 分类知识点
  326. $mastered = [];
  327. $good = [];
  328. $weak = [];
  329. foreach ($attemptedData as $item) {
  330. $level = $item['mastery_level'] ?? 0;
  331. if ($level >= 0.85) {
  332. $mastered[] = $item;
  333. } elseif ($level >= 0.70) {
  334. $good[] = $item;
  335. } else {
  336. $weak[] = $item;
  337. }
  338. }
  339. return [
  340. 'total_knowledge_points' => $total,
  341. 'average_mastery_level' => $average,
  342. 'mastered_knowledge_points' => count($mastered),
  343. 'good_knowledge_points' => count($good),
  344. 'weak_knowledge_points' => count($weak),
  345. 'weak_knowledge_points_list' => $weak,
  346. 'details' => $data
  347. ];
  348. } catch (\Exception $e) {
  349. Log::error('Get Student Mastery Overview Error', [
  350. 'student_id' => $studentId,
  351. 'error' => $e->getMessage()
  352. ]);
  353. return [
  354. 'total_knowledge_points' => 0,
  355. 'average_mastery_level' => 0,
  356. 'mastered_knowledge_points' => 0,
  357. 'good_knowledge_points' => 0,
  358. 'weak_knowledge_points' => 0,
  359. 'weak_knowledge_points_list' => [],
  360. 'details' => []
  361. ];
  362. }
  363. }
  364. /**
  365. * 获取学生技能摘要
  366. */
  367. public function getStudentSkillSummary(string $studentId): array
  368. {
  369. try {
  370. $proficiency = $this->getStudentSkillProficiency($studentId);
  371. // 无论是否有error,都继续处理,返回空数据
  372. $data = $proficiency['data'] ?? [];
  373. $totalSkills = count($data);
  374. $averageLevel = $totalSkills > 0 ? array_sum(array_column($data, 'proficiency_level')) / $totalSkills : 0;
  375. // 计算总答题数
  376. $totalQuestions = 0;
  377. foreach ($data as $skill) {
  378. $totalQuestions += $skill['total_questions_attempted'] ?? 0;
  379. }
  380. return [
  381. 'total_skills' => $totalSkills,
  382. 'average_proficiency_level' => $averageLevel,
  383. 'total_questions_attempted' => $totalQuestions,
  384. 'skill_list' => $data
  385. ];
  386. } catch (\Exception $e) {
  387. Log::warning('Get Student Skill Summary Error', [
  388. 'student_id' => $studentId,
  389. 'error' => $e->getMessage()
  390. ]);
  391. // 发生异常时返回空数据
  392. return [
  393. 'total_skills' => 0,
  394. 'average_proficiency_level' => 0,
  395. 'total_questions_attempted' => 0,
  396. 'skill_list' => []
  397. ];
  398. }
  399. }
  400. /**
  401. * 获取学生预测数据
  402. */
  403. public function getStudentPredictions(string $studentId, int $count = 5): array
  404. {
  405. try {
  406. $response = Http::timeout($this->timeout)
  407. ->get($this->baseUrl . "/api/v1/prediction/student/{$studentId}?count={$count}");
  408. if ($response->successful()) {
  409. $data = $response->json();
  410. $predictions = $data['predictions'] ?? $data['data'] ?? [];
  411. return [
  412. 'predictions' => $predictions
  413. ];
  414. }
  415. return [
  416. 'predictions' => []
  417. ];
  418. } catch (\Exception $e) {
  419. Log::error('Get Student Predictions Error', [
  420. 'student_id' => $studentId,
  421. 'error' => $e->getMessage()
  422. ]);
  423. return [
  424. 'predictions' => []
  425. ];
  426. }
  427. }
  428. /**
  429. * 获取学生学习路径
  430. */
  431. public function getStudentLearningPaths(string $studentId, int $count = 3): array
  432. {
  433. try {
  434. $response = Http::timeout($this->timeout)
  435. ->get($this->baseUrl . "/api/v1/learning-path/student/{$studentId}?count={$count}");
  436. if ($response->successful()) {
  437. $data = $response->json()['data'] ?? [];
  438. return [
  439. 'paths' => $data
  440. ];
  441. }
  442. return [
  443. 'paths' => []
  444. ];
  445. } catch (\Exception $e) {
  446. Log::error('Get Student Learning Paths Error', [
  447. 'student_id' => $studentId,
  448. 'error' => $e->getMessage()
  449. ]);
  450. return [
  451. 'paths' => []
  452. ];
  453. }
  454. }
  455. /**
  456. * 获取预测分析数据
  457. */
  458. public function getPredictionAnalytics(string $studentId): array
  459. {
  460. try {
  461. $predictions = $this->getStudentPredictions($studentId, 10);
  462. if (empty($predictions)) {
  463. return ['accuracy' => 0, 'trend' => 'stable', 'confidence' => 0];
  464. }
  465. $accuracy = 0;
  466. $confidence = 0;
  467. if (!empty($predictions)) {
  468. $accuracy = rand(75, 95); // 模拟准确率
  469. $confidence = rand(70, 90); // 模拟置信度
  470. }
  471. $trend = 'improving'; // improving, stable, declining
  472. return [
  473. 'accuracy' => $accuracy,
  474. 'trend' => $trend,
  475. 'confidence' => $confidence,
  476. 'sample_size' => count($predictions)
  477. ];
  478. } catch (\Exception $e) {
  479. Log::error('Get Prediction Analytics Error', [
  480. 'student_id' => $studentId,
  481. 'error' => $e->getMessage()
  482. ]);
  483. return ['accuracy' => 0, 'trend' => 'stable', 'confidence' => 0];
  484. }
  485. }
  486. /**
  487. * 获取学习路径分析数据
  488. */
  489. public function getLearningPathAnalytics(string $studentId): array
  490. {
  491. try {
  492. $paths = $this->getStudentLearningPaths($studentId, 5);
  493. if (empty($paths)) {
  494. return [
  495. 'active_paths' => 0,
  496. 'completed_paths' => 0,
  497. 'average_efficiency_score' => 0,
  498. 'completion_rate' => 0,
  499. 'average_time' => 0,
  500. 'total_paths' => 0
  501. ];
  502. }
  503. $activePaths = 0;
  504. $completedPaths = 0;
  505. $efficiencyScores = [];
  506. foreach ($paths as $path) {
  507. if (($path['status'] ?? '') === 'active') {
  508. $activePaths++;
  509. }
  510. if (($path['status'] ?? '') === 'completed') {
  511. $completedPaths++;
  512. }
  513. if (isset($path['efficiency_score'])) {
  514. $efficiencyScores[] = $path['efficiency_score'];
  515. }
  516. }
  517. $averageEfficiency = !empty($efficiencyScores)
  518. ? array_sum($efficiencyScores) / count($efficiencyScores)
  519. : rand(60, 85) / 100;
  520. $completionRate = count($paths) > 0
  521. ? ($completedPaths / count($paths)) * 100
  522. : 0;
  523. $averageTime = rand(30, 60); // 模拟平均时间(分钟)
  524. return [
  525. 'active_paths' => $activePaths,
  526. 'completed_paths' => $completedPaths,
  527. 'average_efficiency_score' => $averageEfficiency,
  528. 'completion_rate' => $completionRate,
  529. 'average_time' => $averageTime,
  530. 'total_paths' => count($paths)
  531. ];
  532. } catch (\Exception $e) {
  533. Log::error('Get Learning Path Analytics Error', [
  534. 'student_id' => $studentId,
  535. 'error' => $e->getMessage()
  536. ]);
  537. return [
  538. 'active_paths' => 0,
  539. 'completed_paths' => 0,
  540. 'average_efficiency_score' => 0,
  541. 'completion_rate' => 0,
  542. 'average_time' => 0,
  543. 'total_paths' => 0
  544. ];
  545. }
  546. }
  547. /**
  548. * 快速分数预测
  549. */
  550. public function quickScorePrediction(string $studentId): array
  551. {
  552. Log::info('开始调用快速预测API', ['student_id' => $studentId]);
  553. try {
  554. $response = Http::timeout($this->timeout)
  555. ->post($this->baseUrl . "/api/v1/prediction/student/{$studentId}/quick-prediction", [
  556. 'student_id' => $studentId
  557. ]);
  558. Log::info('快速预测API响应', [
  559. 'student_id' => $studentId,
  560. 'status' => $response->status(),
  561. 'body' => $response->body()
  562. ]);
  563. if ($response->successful()) {
  564. $data = $response->json();
  565. Log::info('快速预测API返回数据', ['student_id' => $studentId, 'data' => $data]);
  566. // API直接返回数据,没有嵌套在'data'键中
  567. $quickPredictionData = $data['quick_prediction'] ?? [];
  568. return [
  569. 'quick_prediction' => [
  570. 'current_score' => $quickPredictionData['current_score'] ?? 70,
  571. 'predicted_score' => $quickPredictionData['predicted_score'] ?? 75,
  572. 'improvement_potential' => $quickPredictionData['improvement_potential'] ?? 5,
  573. 'estimated_study_hours' => $quickPredictionData['estimated_study_hours'] ?? 15,
  574. 'confidence_level' => $quickPredictionData['confidence_level'] ?? 0.75,
  575. 'priority_topics' => $quickPredictionData['priority_topics'] ?? [],
  576. 'recommended_actions' => $quickPredictionData['recommended_actions'] ?? [],
  577. 'weak_knowledge_points_count' => $quickPredictionData['weak_knowledge_points_count'] ?? 0,
  578. 'total_knowledge_points' => $quickPredictionData['total_knowledge_points'] ?? 0
  579. ],
  580. 'predicted_score' => $quickPredictionData['predicted_score'] ?? 75,
  581. 'confidence' => $quickPredictionData['confidence_level'] ? $quickPredictionData['confidence_level'] * 100 : 75,
  582. 'time_estimate' => $quickPredictionData['estimated_study_hours'] ?? 15
  583. ];
  584. }
  585. Log::warning('快速预测API调用失败', [
  586. 'student_id' => $studentId,
  587. 'status' => $response->status(),
  588. 'response' => $response->body()
  589. ]);
  590. return [
  591. 'quick_prediction' => [
  592. 'improvement_potential' => 0,
  593. 'estimated_study_hours' => 0,
  594. 'confidence_level' => 0
  595. ],
  596. 'predicted_score' => 0,
  597. 'confidence' => 0,
  598. 'time_estimate' => 0
  599. ];
  600. } catch (\Exception $e) {
  601. Log::error('Quick Score Prediction Error', [
  602. 'student_id' => $studentId,
  603. 'error' => $e->getMessage()
  604. ]);
  605. return [
  606. 'quick_prediction' => [
  607. 'improvement_potential' => 0,
  608. 'estimated_study_hours' => 0,
  609. 'confidence_level' => 0
  610. ],
  611. 'predicted_score' => 0,
  612. 'confidence' => 0,
  613. 'time_estimate' => 0
  614. ];
  615. }
  616. }
  617. /**
  618. * 推荐学习路径
  619. */
  620. public function recommendLearningPaths(string $studentId, int $count = 3): array
  621. {
  622. try {
  623. $response = Http::timeout($this->timeout)
  624. ->get($this->baseUrl . "/api/v1/learning-path/recommend/{$studentId}?count={$count}");
  625. if ($response->successful()) {
  626. $data = $response->json()['data'] ?? [];
  627. return [
  628. 'recommendations' => $data
  629. ];
  630. }
  631. return [
  632. 'recommendations' => []
  633. ];
  634. } catch (\Exception $e) {
  635. Log::error('Recommend Learning Paths Error', [
  636. 'student_id' => $studentId,
  637. 'error' => $e->getMessage()
  638. ]);
  639. return [
  640. 'recommendations' => []
  641. ];
  642. }
  643. }
  644. /**
  645. * 重新计算掌握度
  646. */
  647. public function recalculateMastery(string $studentId, string $kpCode): bool
  648. {
  649. try {
  650. $response = Http::timeout($this->timeout)
  651. ->post($this->baseUrl . "/api/v1/mastery/recalculate/{$studentId}", [
  652. 'student_id' => $studentId,
  653. 'kp_code' => $kpCode
  654. ]);
  655. return $response->successful();
  656. } catch (\Exception $e) {
  657. Log::error('Recalculate Mastery Error', [
  658. 'student_id' => $studentId,
  659. 'kp_code' => $kpCode,
  660. 'error' => $e->getMessage()
  661. ]);
  662. return false;
  663. }
  664. }
  665. /**
  666. * 批量更新技能熟练度
  667. */
  668. public function batchUpdateSkillProficiency(string $studentId): bool
  669. {
  670. try {
  671. $response = Http::timeout($this->timeout)
  672. ->post($this->baseUrl . "/api/v1/skill/proficiency/student/{$studentId}/batch-update", [
  673. 'student_id' => $studentId
  674. ]);
  675. return $response->successful();
  676. } catch (\Exception $e) {
  677. Log::error('Batch Update Skill Proficiency Error', [
  678. 'student_id' => $studentId,
  679. 'error' => $e->getMessage()
  680. ]);
  681. return false;
  682. }
  683. }
  684. /**
  685. * 清空学生所有答题数据
  686. */
  687. public function clearStudentData(string $studentId): bool
  688. {
  689. try {
  690. // 清空LearningAnalytics中的数据(通过API)
  691. $response = Http::timeout($this->timeout)
  692. ->delete($this->baseUrl . "/api/v1/student/{$studentId}/clear");
  693. if (!$response->successful()) {
  694. Log::error('Clear LearningAnalytics Data Failed', [
  695. 'student_id' => $studentId,
  696. 'status' => $response->status(),
  697. 'response' => $response->body()
  698. ]);
  699. }
  700. // 清空MySQL中的数据
  701. $this->clearStudentMySQLData($studentId);
  702. Log::info('Student Data Cleared Successfully', [
  703. 'student_id' => $studentId,
  704. 'api_success' => $response->successful()
  705. ]);
  706. return true;
  707. } catch (\Exception $e) {
  708. Log::error('Clear Student Data Error', [
  709. 'student_id' => $studentId,
  710. 'error' => $e->getMessage()
  711. ]);
  712. // 即使API失败,也要尝试清空本地数据
  713. try {
  714. $this->clearStudentMySQLData($studentId);
  715. return true;
  716. } catch (\Exception $localError) {
  717. Log::error('Clear Local Data Also Failed', [
  718. 'student_id' => $studentId,
  719. 'error' => $localError->getMessage()
  720. ]);
  721. return false;
  722. }
  723. }
  724. }
  725. /**
  726. * 清空学生MySQL中的答题数据
  727. */
  728. private function clearStudentMySQLData(string $studentId): void
  729. {
  730. try {
  731. // 清空student_exercises表
  732. DB::table('student_exercises')
  733. ->where('student_id', $studentId)
  734. ->delete();
  735. // 清空student_mastery表
  736. DB::table('student_mastery')
  737. ->where('student_id', $studentId)
  738. ->delete();
  739. Log::info('Student MySQL Data Cleared', [
  740. 'student_id' => $studentId
  741. ]);
  742. } catch (\Exception $e) {
  743. Log::error('Clear Student MySQL Data Error', [
  744. 'student_id' => $studentId,
  745. 'error' => $e->getMessage()
  746. ]);
  747. throw $e; // 重新抛出异常,让上层处理
  748. }
  749. }
  750. /**
  751. * 获取学生列表(供智能出卷使用)
  752. */
  753. public function getStudentsList(): array
  754. {
  755. try {
  756. $response = Http::timeout($this->timeout)
  757. ->get($this->baseUrl . '/api/v1/students/list');
  758. if ($response->successful()) {
  759. return $response->json('data', []);
  760. }
  761. // 如果API失败,尝试从MySQL直接读取
  762. return $this->getStudentsFromMySQL();
  763. } catch (\Exception $e) {
  764. Log::error('Get Students List Error', [
  765. 'error' => $e->getMessage()
  766. ]);
  767. // 返回模拟数据
  768. return [
  769. ['student_id' => 'stu_001', 'name' => '张三'],
  770. ['student_id' => 'stu_002', 'name' => '李四'],
  771. ['student_id' => 'stu_003', 'name' => '王五'],
  772. ];
  773. }
  774. }
  775. /**
  776. * 从MySQL获取学生列表
  777. */
  778. private function getStudentsFromMySQL(): array
  779. {
  780. try {
  781. return DB::table('students')
  782. ->select('student_id', 'name')
  783. ->limit(100)
  784. ->get()
  785. ->toArray();
  786. } catch (\Exception $e) {
  787. Log::error('Get Students From MySQL Error', [
  788. 'error' => $e->getMessage()
  789. ]);
  790. return [];
  791. }
  792. }
  793. /**
  794. * 获取学生薄弱点列表
  795. */
  796. public function getStudentWeaknesses(string $studentId, int $limit = 10): array
  797. {
  798. try {
  799. // 使用正确的API路径:/api/v1/student/{student_id}/weak-points
  800. $response = Http::timeout($this->timeout)
  801. ->get($this->baseUrl . "/api/v1/student/{$studentId}/weak-points");
  802. if ($response->successful()) {
  803. $data = $response->json('data', []);
  804. $weakPoints = $data['weak_points'] ?? [];
  805. // 转换为统一的格式
  806. return array_map(function ($item) use ($studentId) {
  807. return [
  808. 'kp_code' => $item['kp'] ?? '',
  809. 'kp_name' => $item['kp'] ?? '',
  810. 'mastery' => $item['mastery_level'] ?? 0,
  811. 'stability' => 0.5, // 默认稳定性
  812. 'weakness_level' => 1.0 - ($item['mastery_level'] ?? 0.5),
  813. 'practice_count' => $item['practice_count'] ?? 0,
  814. 'success_rate' => $item['success_rate'] ?? 0,
  815. 'priority' => $item['priority'] ?? '中',
  816. 'suggested_questions' => $item['suggested_questions'] ?? 0
  817. ];
  818. }, $weakPoints);
  819. }
  820. Log::warning('LearningAnalytics weaknesses API失败,使用本地MySQL数据', [
  821. 'student_id' => $studentId,
  822. 'status' => $response->status()
  823. ]);
  824. // API失败时,从MySQL直接查询
  825. return $this->getStudentWeaknessesFromMySQL($studentId, $limit);
  826. } catch (\Exception $e) {
  827. Log::error('Get Student Weaknesses Error', [
  828. 'student_id' => $studentId,
  829. 'error' => $e->getMessage()
  830. ]);
  831. // 发生异常时,返回空数组,让前端可以继续使用默认值
  832. return [];
  833. }
  834. }
  835. /**
  836. * 从MySQL获取学生薄弱点
  837. */
  838. private function getStudentWeaknessesFromMySQL(string $studentId, int $limit = 10): array
  839. {
  840. try {
  841. $weaknesses = DB::table('student_mastery as sm')
  842. ->join('knowledge_points as kp', 'sm.kp', '=', 'kp.kp')
  843. ->where('sm.student_id', $studentId)
  844. ->where('sm.mastery', '<', 0.7) // 掌握度低于70%视为薄弱点
  845. ->orderBy('sm.mastery', 'asc')
  846. ->limit($limit)
  847. ->select([
  848. 'sm.kp as kp_code',
  849. 'kp.cn_name as kp_name',
  850. 'sm.mastery',
  851. 'sm.stability'
  852. ])
  853. ->get()
  854. ->toArray();
  855. return array_map(function ($item) {
  856. return [
  857. 'kp_code' => $item->kp_code,
  858. 'kp_name' => $item->kp_name,
  859. 'mastery' => (float) $item->mastery,
  860. 'stability' => (float) $item->stability,
  861. 'weakness_level' => 1.0 - (float) $item->mastery // 薄弱程度
  862. ];
  863. }, $weaknesses);
  864. } catch (\Exception $e) {
  865. Log::error('Get Student Weaknesses From MySQL Error', [
  866. 'student_id' => $studentId,
  867. 'error' => $e->getMessage()
  868. ]);
  869. return [];
  870. }
  871. }
  872. /**
  873. * 智能出卷:根据学生掌握度智能选择题目
  874. */
  875. public function generateIntelligentExam(array $params): array
  876. {
  877. try {
  878. $studentId = $params['student_id'] ?? null;
  879. $totalQuestions = $params['total_questions'] ?? 20;
  880. $kpCodes = $params['kp_codes'] ?? [];
  881. $skills = $params['skills'] ?? [];
  882. $questionTypeRatio = $params['question_type_ratio'] ?? [
  883. '选择题' => 40,
  884. '填空题' => 30,
  885. '解答题' => 30,
  886. ];
  887. $difficultyRatio = $params['difficulty_ratio'] ?? [
  888. '基础' => 50,
  889. '中等' => 35,
  890. '拔高' => 15,
  891. ];
  892. // 1. 如果指定了学生,获取学生的薄弱点
  893. $weaknessFilter = [];
  894. if ($studentId) {
  895. $weaknesses = $this->getStudentWeaknesses($studentId, 20);
  896. $weaknessFilter = array_column($weaknesses, 'kp_code');
  897. // 如果用户没有指定知识点,使用学生的薄弱点
  898. if (empty($kpCodes)) {
  899. $kpCodes = $weaknessFilter;
  900. }
  901. }
  902. // 如果仍然没有知识点(例如新学生无薄弱点),根据年级从知识图谱获取知识点
  903. if (empty($kpCodes)) {
  904. $filters = [];
  905. if ($studentId) {
  906. $student = \App\Models\Student::find($studentId);
  907. if ($student && $student->grade) {
  908. $grade = $student->grade;
  909. $standardizedGrade = $grade;
  910. // 标准化年级名称并更新数据库
  911. if ($grade === '初一') {
  912. $standardizedGrade = '七年级';
  913. } elseif ($grade === '初二') {
  914. $standardizedGrade = '八年级';
  915. } elseif ($grade === '初三') {
  916. $standardizedGrade = '九年级';
  917. }
  918. if ($standardizedGrade !== $grade) {
  919. $student->grade = $standardizedGrade;
  920. $student->save();
  921. Log::info('Standardized student grade', ['student_id' => $studentId, 'old' => $grade, 'new' => $standardizedGrade]);
  922. $grade = $standardizedGrade;
  923. }
  924. // 映射年级到学段 (phase)
  925. if (str_contains($grade, '初') || str_contains($grade, '七年级') || str_contains($grade, '八年级') || str_contains($grade, '九年级')) {
  926. $filters['phase'] = '初中';
  927. } elseif (str_contains($grade, '高')) {
  928. $filters['phase'] = '高中';
  929. }
  930. }
  931. }
  932. // 调用API获取过滤后的知识点
  933. $filteredKps = $this->getKnowledgePoints($filters);
  934. if (!empty($filteredKps)) {
  935. // 随机选择 5 个知识点
  936. $kpKeys = array_column($filteredKps, 'kp_code');
  937. if (empty($kpKeys)) {
  938. $kpKeys = array_column($filteredKps, 'code');
  939. }
  940. if (!empty($kpKeys)) {
  941. $randomKeys = array_rand(array_flip($kpKeys), min(5, count($kpKeys)));
  942. $kpCodes = is_array($randomKeys) ? $randomKeys : [$randomKeys];
  943. Log::info('Randomly selected KPs for student based on grade (API)', [
  944. 'student_id' => $studentId,
  945. 'grade' => $student->grade ?? 'unknown',
  946. 'filters' => $filters,
  947. 'kps' => $kpCodes
  948. ]);
  949. }
  950. }
  951. }
  952. // 2. 调用题库API获取符合条件的所有题目
  953. $allQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId);
  954. if (empty($allQuestions)) {
  955. // 根据是否有选择的知识点给出不同的错误信息
  956. if (empty($kpCodes)) {
  957. $message = '未选择知识点,无法生成试卷。请先选择知识点或选择学生以获取薄弱点推荐。';
  958. } else {
  959. $message = '题库中暂无可用题目。您可以选择其他知识点,或点击"生成练习题"按钮先补充题库。';
  960. }
  961. Log::warning('智能出卷失败 - 未找到题目', [
  962. 'student_id' => $studentId,
  963. 'selected_kp_codes' => $kpCodes,
  964. 'message' => $message
  965. ]);
  966. return [
  967. 'success' => false,
  968. 'message' => $message,
  969. 'questions' => []
  970. ];
  971. }
  972. // 3. 根据掌握度对题目进行筛选和排序
  973. $selectedQuestions = $this->selectQuestionsByMastery(
  974. $allQuestions,
  975. $studentId,
  976. $totalQuestions,
  977. $questionTypeRatio,
  978. $difficultyRatio,
  979. $weaknessFilter
  980. );
  981. if (empty($selectedQuestions)) {
  982. return [
  983. 'success' => false,
  984. 'message' => '题目筛选失败',
  985. 'questions' => []
  986. ];
  987. }
  988. return [
  989. 'success' => true,
  990. 'message' => '智能出卷成功',
  991. 'questions' => $selectedQuestions,
  992. 'stats' => [
  993. 'total_selected' => count($selectedQuestions),
  994. 'source_questions' => count($allQuestions),
  995. 'weakness_targeted' => $studentId ? count(array_intersect(array_column($selectedQuestions, 'kp_code'), $weaknessFilter)) : 0
  996. ]
  997. ];
  998. } catch (\Exception $e) {
  999. Log::error('Generate Intelligent Exam Error', [
  1000. 'error' => $e->getMessage(),
  1001. 'trace' => $e->getTraceAsString()
  1002. ]);
  1003. return [
  1004. 'success' => false,
  1005. 'message' => '智能出卷异常: ' . $e->getMessage(),
  1006. 'questions' => []
  1007. ];
  1008. }
  1009. }
  1010. /**
  1011. * 从题库获取题目
  1012. */
  1013. private function getQuestionsFromBank(array $kpCodes, array $skills, ?string $studentId): array
  1014. {
  1015. try {
  1016. // 构建查询参数
  1017. $params = [
  1018. 'kp_codes' => implode(',', $kpCodes),
  1019. 'limit' => 1000 // 获取足够多的题目用于筛选
  1020. ];
  1021. if (!empty($skills)) {
  1022. $params['skills'] = implode(',', $skills);
  1023. }
  1024. if ($studentId) {
  1025. $params['exclude_student_questions'] = $studentId; // 过滤学生做过的题目
  1026. }
  1027. // 调用QuestionBank API
  1028. // 使用 QuestionBankService 获取题目 (使用 filterQuestions 方法以支持 kp_codes)
  1029. // 从容器动态获取实例
  1030. if (!$this->questionBankService) {
  1031. $this->questionBankService = app(QuestionBankService::class);
  1032. }
  1033. $response = $this->questionBankService->filterQuestions($params);
  1034. if (!empty($response['data'])) {
  1035. return $response['data'];
  1036. }
  1037. Log::warning('Get Questions From Bank Failed or Empty', [
  1038. 'params' => $params,
  1039. 'response' => $response
  1040. ]);
  1041. return [];
  1042. } catch (\Exception $e) {
  1043. Log::error('Get Questions From Bank Error', [
  1044. 'error' => $e->getMessage()
  1045. ]);
  1046. }
  1047. return [];
  1048. }
  1049. /**
  1050. * 根据学生掌握度筛选题目
  1051. */
  1052. private function selectQuestionsByMastery(
  1053. array $questions,
  1054. ?string $studentId,
  1055. int $totalQuestions,
  1056. array $questionTypeRatio,
  1057. array $difficultyRatio,
  1058. array $weaknessFilter
  1059. ): array {
  1060. // 1. 按知识点分组
  1061. $questionsByKp = [];
  1062. foreach ($questions as $question) {
  1063. $kpCode = $question['kp_code'] ?? '';
  1064. if (!isset($questionsByKp[$kpCode])) {
  1065. $questionsByKp[$kpCode] = [];
  1066. }
  1067. $questionsByKp[$kpCode][] = $question;
  1068. }
  1069. // 2. 为每个知识点计算权重
  1070. $kpWeights = [];
  1071. foreach (array_keys($questionsByKp) as $kpCode) {
  1072. if ($studentId) {
  1073. // 获取学生对该知识点的掌握度
  1074. $mastery = $this->getStudentKpMastery($studentId, $kpCode);
  1075. // 薄弱点权重更高
  1076. if (in_array($kpCode, $weaknessFilter)) {
  1077. $kpWeights[$kpCode] = 2.0; // 薄弱点权重翻倍
  1078. } else {
  1079. // 掌握度越低,权重越高
  1080. $kpWeights[$kpCode] = 1.0 + (1.0 - $mastery) * 1.5;
  1081. }
  1082. } else {
  1083. $kpWeights[$kpCode] = 1.0; // 未指定学生时平均分配
  1084. }
  1085. }
  1086. // 3. 按权重分配题目数量
  1087. $totalWeight = array_sum($kpWeights);
  1088. $selectedQuestions = [];
  1089. foreach ($questionsByKp as $kpCode => $kpQuestions) {
  1090. // 计算该知识点应该选择的题目数
  1091. $kpQuestionCount = max(1, round(($totalQuestions * $kpWeights[$kpCode]) / $totalWeight));
  1092. // 打乱题目顺序(避免固定模式)
  1093. shuffle($kpQuestions);
  1094. // 选择题目
  1095. $selectedFromKp = array_slice($kpQuestions, 0, $kpQuestionCount);
  1096. $selectedQuestions = array_merge($selectedQuestions, $selectedFromKp);
  1097. }
  1098. // 4. 如果题目过多,按权重排序后截取
  1099. if (count($selectedQuestions) > $totalQuestions) {
  1100. usort($selectedQuestions, function ($a, $b) use ($kpWeights) {
  1101. $weightA = $kpWeights[$a['kp_code']] ?? 1.0;
  1102. $weightB = $kpWeights[$b['kp_code']] ?? 1.0;
  1103. return $weightB <=> $weightA;
  1104. });
  1105. $selectedQuestions = array_slice($selectedQuestions, 0, $totalQuestions);
  1106. }
  1107. // 5. 按题型和难度进行微调
  1108. return $this->adjustQuestionsByRatio($selectedQuestions, $questionTypeRatio, $difficultyRatio);
  1109. }
  1110. /**
  1111. * 获取学生对特定知识点的掌握度
  1112. */
  1113. private function getStudentKpMastery(string $studentId, string $kpCode): float
  1114. {
  1115. try {
  1116. $mastery = DB::table('student_mastery')
  1117. ->where('student_id', $studentId)
  1118. ->where('kp', $kpCode)
  1119. ->value('mastery');
  1120. return $mastery ? (float) $mastery : 0.5; // 默认0.5(中等掌握度)
  1121. } catch (\Exception $e) {
  1122. Log::error('Get Student Kp Mastery Error', [
  1123. 'student_id' => $studentId,
  1124. 'kp_code' => $kpCode,
  1125. 'error' => $e->getMessage()
  1126. ]);
  1127. return 0.5;
  1128. }
  1129. }
  1130. /**
  1131. * 根据题型和难度配比调整题目
  1132. */
  1133. private function adjustQuestionsByRatio(array $questions, array $typeRatio, array $difficultyRatio): array
  1134. {
  1135. // 这里可以实现更精细的调整逻辑
  1136. // 目前先返回原始题目,后续可以基于question_type和difficulty字段进行调整
  1137. return $questions;
  1138. }
  1139. }