SimulatedGrading.php 43 KB

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