ExamTypeStrategy.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Log;
  4. use App\Models\StudentKnowledgeMastery;
  5. use App\Models\MistakeRecord;
  6. use App\Models\KnowledgePoint;
  7. class ExamTypeStrategy
  8. {
  9. protected QuestionExpansionService $questionExpansionService;
  10. public function __construct(QuestionExpansionService $questionExpansionService)
  11. {
  12. $this->questionExpansionService = $questionExpansionService;
  13. }
  14. /**
  15. * 根据组卷类型构建参数
  16. */
  17. public function buildParams(array $baseParams, string $examType): array
  18. {
  19. Log::info('ExamTypeStrategy: 构建组卷参数', [
  20. 'exam_type' => $examType,
  21. 'base_params_keys' => array_keys($baseParams)
  22. ]);
  23. return match($examType) {
  24. 'diagnostic' => $this->buildDiagnosticParams($baseParams),
  25. 'practice' => $this->buildPracticeParams($baseParams),
  26. 'mistake' => $this->buildMistakeParams($baseParams),
  27. 'textbook' => $this->buildTextbookParams($baseParams),
  28. 'knowledge' => $this->buildKnowledgeParams($baseParams),
  29. 'knowledge_points' => $this->buildKnowledgePointsParams($baseParams),
  30. default => $this->buildGeneralParams($baseParams)
  31. };
  32. }
  33. /**
  34. * 通用智能出卷(原有行为)
  35. */
  36. private function buildGeneralParams(array $params): array
  37. {
  38. Log::info('ExamTypeStrategy: 通用智能出卷参数', $params);
  39. // 返回原始参数,不做特殊处理
  40. return $params;
  41. }
  42. /**
  43. * 摸底测试:评估当前水平
  44. */
  45. private function buildDiagnosticParams(array $params): array
  46. {
  47. Log::info('ExamTypeStrategy: 构建摸底测试参数', $params);
  48. // 摸底测试:平衡所有难度,覆盖多个知识点
  49. $enhanced = array_merge($params, [
  50. // 难度配比:相对平衡,基础题稍多
  51. 'difficulty_ratio' => [
  52. '基础' => 40,
  53. '中等' => 40,
  54. '拔高' => 20,
  55. ],
  56. // 题型配比:选择题多一些,便于快速评估
  57. 'question_type_ratio' => [
  58. '选择题' => 50,
  59. '填空题' => 25,
  60. '解答题' => 25,
  61. ],
  62. // 确保覆盖多个知识点
  63. 'kp_codes' => $this->expandKpCodesForDiagnostic($params['kp_codes'] ?? []),
  64. // 摸底测试名称
  65. 'paper_name' => $params['paper_name'] ?? ('摸底测试_' . now()->format('Ymd_His')),
  66. ]);
  67. Log::info('ExamTypeStrategy: 摸底测试参数构建完成', [
  68. 'difficulty_ratio' => $enhanced['difficulty_ratio'],
  69. 'question_type_ratio' => $enhanced['question_type_ratio'],
  70. 'kp_codes_count' => count($enhanced['kp_codes'])
  71. ]);
  72. return $enhanced;
  73. }
  74. /**
  75. * 错题:针对薄弱点强化
  76. * 使用 QuestionExpansionService 按优先级扩展题目
  77. */
  78. private function buildMistakeParams(array $params): array
  79. {
  80. Log::info('ExamTypeStrategy: 构建错题参数', $params);
  81. $studentId = $params['student_id'] ?? null;
  82. $totalQuestions = $params['total_questions'] ?? 20;
  83. $mistakeOptions = $params['mistake_options'] ?? [];
  84. $weaknessThreshold = $mistakeOptions['weakness_threshold'] ?? 0.7;
  85. $intensity = $mistakeOptions['intensity'] ?? 'medium';
  86. $focusWeaknesses = $mistakeOptions['focus_weaknesses'] ?? true;
  87. // 根据强度调整难度配比
  88. $difficultyRatio = match($intensity) {
  89. 'low' => [
  90. '基础' => 60,
  91. '中等' => 35,
  92. '拔高' => 5,
  93. ],
  94. 'medium' => [
  95. '基础' => 45,
  96. '中等' => 40,
  97. '拔高' => 15,
  98. ],
  99. 'high' => [
  100. '基础' => 30,
  101. '中等' => 45,
  102. '拔高' => 25,
  103. ],
  104. default => [
  105. '基础' => 45,
  106. '中等' => 40,
  107. '拔高' => 15,
  108. ]
  109. };
  110. // 获取学生薄弱点
  111. $weaknessFilter = [];
  112. if ($studentId && $focusWeaknesses) {
  113. $weaknessFilter = $this->getStudentWeaknesses($studentId, $weaknessThreshold);
  114. Log::info('ExamTypeStrategy: 获取到学生薄弱点', [
  115. 'student_id' => $studentId,
  116. 'weakness_threshold' => $weaknessThreshold,
  117. 'weakness_count' => count($weaknessFilter)
  118. ]);
  119. }
  120. // 使用 QuestionExpansionService 按优先级扩展题目
  121. $questionStrategy = $this->questionExpansionService->expandQuestions(
  122. $params,
  123. $studentId,
  124. $weaknessFilter,
  125. $totalQuestions
  126. );
  127. // 获取错题知识点
  128. $mistakeKnowledgePoints = [];
  129. if ($studentId && !empty($questionStrategy['mistake_question_ids'])) {
  130. $mistakeRecords = MistakeRecord::forStudent($studentId)
  131. ->whereIn('question_id', $questionStrategy['mistake_question_ids'])
  132. ->select(['knowledge_point'])
  133. ->get();
  134. $mistakeKnowledgePoints = array_unique(array_filter($mistakeRecords->pluck('knowledge_point')->toArray()));
  135. Log::info('ExamTypeStrategy: 获取错题知识点', [
  136. 'knowledge_points' => $mistakeKnowledgePoints
  137. ]);
  138. }
  139. // 获取扩展统计
  140. $expansionStats = $this->questionExpansionService->getExpansionStats($questionStrategy);
  141. $enhanced = array_merge($params, [
  142. 'difficulty_ratio' => $difficultyRatio,
  143. 'mistake_ids' => $questionStrategy['mistake_ids'],
  144. 'mistake_question_ids' => $questionStrategy['mistake_question_ids'],
  145. // 错题回顾的知识点优先级
  146. 'priority_knowledge_points' => array_merge(
  147. array_values($mistakeKnowledgePoints), // 错题知识点优先
  148. array_column($weaknessFilter, 'kp_code') // 然后是薄弱点
  149. ),
  150. // 错题回顾更注重针对性
  151. 'question_type_ratio' => [
  152. '选择题' => 35,
  153. '填空题' => 30,
  154. '解答题' => 35,
  155. ],
  156. 'paper_name' => $params['paper_name'] ?? ('错题_' . now()->format('Ymd_His')),
  157. // 标记这是错题,用于后续处理
  158. 'is_mistake_exam' => true,
  159. 'weakness_filter' => $weaknessFilter,
  160. // 题目扩展统计
  161. 'question_expansion_stats' => $expansionStats
  162. ]);
  163. Log::info('ExamTypeStrategy: 错题参数构建完成', [
  164. 'intensity' => $intensity,
  165. 'total_questions_needed' => $totalQuestions,
  166. 'mistake_question_ids_count' => count($enhanced['mistake_question_ids']),
  167. 'priority_knowledge_points_count' => count($enhanced['priority_knowledge_points']),
  168. 'question_expansion_stats' => $enhanced['question_expansion_stats'],
  169. 'weakness_count' => count($weaknessFilter)
  170. ]);
  171. return $enhanced;
  172. }
  173. /**
  174. * 专项练习:针对特定技能或知识点练习
  175. */
  176. private function buildPracticeParams(array $params): array
  177. {
  178. Log::info('ExamTypeStrategy: 构建专项练习参数', $params);
  179. $studentId = $params['student_id'] ?? null;
  180. $practiceOptions = $params['practice_options'] ?? [];
  181. $weaknessThreshold = $practiceOptions['weakness_threshold'] ?? 0.7;
  182. $intensity = $practiceOptions['intensity'] ?? 'medium';
  183. $focusWeaknesses = $practiceOptions['focus_weaknesses'] ?? true;
  184. // 根据强度调整难度配比
  185. $difficultyRatio = match($intensity) {
  186. 'low' => [
  187. '基础' => 60,
  188. '中等' => 35,
  189. '拔高' => 5,
  190. ],
  191. 'medium' => [
  192. '基础' => 45,
  193. '中等' => 40,
  194. '拔高' => 15,
  195. ],
  196. 'high' => [
  197. '基础' => 30,
  198. '中等' => 45,
  199. '拔高' => 25,
  200. ],
  201. default => [
  202. '基础' => 45,
  203. '中等' => 40,
  204. '拔高' => 15,
  205. ]
  206. };
  207. // 获取学生薄弱点
  208. $weaknessFilter = [];
  209. if ($studentId && $focusWeaknesses) {
  210. $weaknessFilter = $this->getStudentWeaknesses($studentId, $weaknessThreshold);
  211. Log::info('ExamTypeStrategy: 获取到学生薄弱点', [
  212. 'student_id' => $studentId,
  213. 'weakness_threshold' => $weaknessThreshold,
  214. 'weakness_count' => count($weaknessFilter)
  215. ]);
  216. }
  217. // 优先使用薄弱点知识点,如果没有则使用用户选择的知识点
  218. $kpCodes = $params['kp_codes'] ?? [];
  219. if ($studentId && empty($kpCodes) && !empty($weaknessFilter)) {
  220. $kpCodes = array_column($weaknessFilter, 'kp_code');
  221. Log::info('ExamTypeStrategy: 使用薄弱点作为知识点', [
  222. 'kp_codes' => $kpCodes
  223. ]);
  224. }
  225. $enhanced = array_merge($params, [
  226. 'difficulty_ratio' => $difficultyRatio,
  227. 'kp_codes' => $kpCodes,
  228. // 专项练习更注重题型覆盖
  229. 'question_type_ratio' => [
  230. '选择题' => 40,
  231. '填空题' => 30,
  232. '解答题' => 30,
  233. ],
  234. 'paper_name' => $params['paper_name'] ?? ('专项练习_' . now()->format('Ymd_His')),
  235. // 标记这是专项练习,用于后续处理
  236. 'is_practice_exam' => true,
  237. 'weakness_filter' => $weaknessFilter,
  238. ]);
  239. Log::info('ExamTypeStrategy: 专项练习参数构建完成', [
  240. 'intensity' => $intensity,
  241. 'difficulty_ratio' => $enhanced['difficulty_ratio'],
  242. 'kp_codes_count' => count($enhanced['kp_codes']),
  243. 'weakness_count' => count($weaknessFilter)
  244. ]);
  245. return $enhanced;
  246. }
  247. /**
  248. * 教材同步:按教材章节出题
  249. */
  250. private function buildTextbookParams(array $params): array
  251. {
  252. Log::info('ExamTypeStrategy: 构建教材同步参数', $params);
  253. // 教材同步:按章节顺序,难度递增
  254. $textbookOptions = $params['textbook_options'] ?? [];
  255. $enhanced = array_merge($params, [
  256. // 教材同步:基础和中等题为主
  257. 'difficulty_ratio' => [
  258. '基础' => 50,
  259. '中等' => 40,
  260. '拔高' => 10,
  261. ],
  262. 'question_type_ratio' => [
  263. '选择题' => 40,
  264. '填空题' => 30,
  265. '解答题' => 30,
  266. ],
  267. 'paper_name' => $params['paper_name'] ?? ('教材同步_' . now()->format('Ymd_His')),
  268. 'textbook_options' => $textbookOptions,
  269. ]);
  270. return $enhanced;
  271. }
  272. /**
  273. * 知识点专练:单个或少量知识点深练
  274. */
  275. private function buildKnowledgeParams(array $params): array
  276. {
  277. Log::info('ExamTypeStrategy: 构建知识点专练参数', $params);
  278. // 知识点专练:深度挖掘,多角度考查
  279. $knowledgeOptions = $params['knowledge_options'] ?? [];
  280. $enhanced = array_merge($params, [
  281. // 知识点专练:难度分布更均匀
  282. 'difficulty_ratio' => [
  283. '基础' => 35,
  284. '中等' => 45,
  285. '拔高' => 20,
  286. ],
  287. 'question_type_ratio' => [
  288. '选择题' => 30,
  289. '填空题' => 35,
  290. '解答题' => 35,
  291. ],
  292. 'paper_name' => $params['paper_name'] ?? ('知识点专练_' . now()->format('Ymd_His')),
  293. 'knowledge_options' => $knowledgeOptions,
  294. ]);
  295. return $enhanced;
  296. }
  297. /**
  298. * 按知识点组卷:根据指定知识点数组智能选题
  299. * 优先级策略:
  300. * 1. 直接关联知识点题目(来自输入数组)
  301. * 2. 相同知识点其他题目
  302. * 3. 子知识点题目(下探1层)
  303. * 4. 薄弱点题目比例调整
  304. * 5. 子知识点题目(下探2层)
  305. */
  306. private function buildKnowledgePointsParams(array $params): array
  307. {
  308. Log::info('ExamTypeStrategy: 构建按知识点组卷参数', $params);
  309. $studentId = $params['student_id'] ?? null;
  310. $totalQuestions = $params['total_questions'] ?? 20;
  311. $knowledgePointsOptions = $params['knowledge_points_options'] ?? [];
  312. $weaknessThreshold = $knowledgePointsOptions['weakness_threshold'] ?? 0.7;
  313. $focusWeaknesses = $knowledgePointsOptions['focus_weaknesses'] ?? true;
  314. $intensity = $knowledgePointsOptions['intensity'] ?? 'medium';
  315. // 获取用户指定的知识点数组
  316. $targetKnowledgePoints = $params['kp_codes'] ?? [];
  317. if (empty($targetKnowledgePoints)) {
  318. Log::warning('ExamTypeStrategy: 未指定知识点数组,使用默认策略');
  319. return $this->buildGeneralParams($params);
  320. }
  321. Log::info('ExamTypeStrategy: 目标知识点数组', [
  322. 'target_knowledge_points' => $targetKnowledgePoints,
  323. 'count' => count($targetKnowledgePoints)
  324. ]);
  325. // 根据强度调整难度配比
  326. $difficultyRatio = match($intensity) {
  327. 'low' => [
  328. '基础' => 60,
  329. '中等' => 35,
  330. '拔高' => 5,
  331. ],
  332. 'medium' => [
  333. '基础' => 45,
  334. '中等' => 40,
  335. '拔高' => 15,
  336. ],
  337. 'high' => [
  338. '基础' => 30,
  339. '中等' => 45,
  340. '拔高' => 25,
  341. ],
  342. default => [
  343. '基础' => 45,
  344. '中等' => 40,
  345. '拔高' => 15,
  346. ]
  347. };
  348. // 获取学生薄弱点(用于判断目标知识点是否为薄弱点)
  349. $weaknessFilter = [];
  350. if ($studentId && $focusWeaknesses) {
  351. $weaknessFilter = $this->getStudentWeaknesses($studentId, $weaknessThreshold);
  352. Log::info('ExamTypeStrategy: 获取到学生薄弱点', [
  353. 'student_id' => $studentId,
  354. 'weakness_threshold' => $weaknessThreshold,
  355. 'weakness_count' => count($weaknessFilter)
  356. ]);
  357. }
  358. // 检查目标知识点中哪些是薄弱点
  359. $weaknessKpCodes = array_column($weaknessFilter, 'kp_code');
  360. $targetWeaknessKps = array_intersect($targetKnowledgePoints, $weaknessKpCodes);
  361. Log::info('ExamTypeStrategy: 目标知识点中的薄弱点', [
  362. 'target_weakness_kps' => $targetWeaknessKps,
  363. 'weakness_count' => count($targetWeaknessKps)
  364. ]);
  365. // 使用 QuestionExpansionService 按知识点优先级扩展题目
  366. // 修改 expandQuestions 支持直接传入知识点数组
  367. $questionStrategy = $this->questionExpansionService->expandQuestionsByKnowledgePoints(
  368. $params,
  369. $studentId,
  370. $targetKnowledgePoints,
  371. $weaknessFilter,
  372. $totalQuestions
  373. );
  374. // 获取扩展统计
  375. $expansionStats = $this->questionExpansionService->getExpansionStats($questionStrategy);
  376. $enhanced = array_merge($params, [
  377. 'difficulty_ratio' => $difficultyRatio,
  378. 'kp_codes' => $targetKnowledgePoints, // 确保使用目标知识点
  379. 'mistake_question_ids' => $questionStrategy['mistake_question_ids'] ?? [],
  380. // 优先级知识点:目标知识点 + 薄弱点
  381. 'priority_knowledge_points' => array_merge(
  382. array_values($targetKnowledgePoints),
  383. array_column($weaknessFilter, 'kp_code')
  384. ),
  385. 'question_type_ratio' => [
  386. '选择题' => 35,
  387. '填空题' => 30,
  388. '解答题' => 35,
  389. ],
  390. 'paper_name' => $params['paper_name'] ?? ('知识点组卷_' . now()->format('Ymd_His')),
  391. // 标记这是按知识点组卷,用于后续处理
  392. 'is_knowledge_points_exam' => true,
  393. 'weakness_filter' => $weaknessFilter,
  394. // 目标知识点中的薄弱点(用于调整题目数量)
  395. 'target_weakness_kps' => array_values($targetWeaknessKps),
  396. // 题目扩展统计
  397. 'question_expansion_stats' => $expansionStats
  398. ]);
  399. Log::info('ExamTypeStrategy: 按知识点组卷参数构建完成', [
  400. 'intensity' => $intensity,
  401. 'target_knowledge_points_count' => count($targetKnowledgePoints),
  402. 'target_weakness_kps_count' => count($targetWeaknessKps),
  403. 'mistake_question_ids_count' => count($enhanced['mistake_question_ids']),
  404. 'priority_knowledge_points_count' => count($enhanced['priority_knowledge_points']),
  405. 'question_expansion_stats' => $enhanced['question_expansion_stats'],
  406. 'weakness_count' => count($weaknessFilter)
  407. ]);
  408. return $enhanced;
  409. }
  410. /**
  411. * 为摸底测试扩展知识点(确保覆盖全面)
  412. */
  413. private function expandKpCodesForDiagnostic(array $kpCodes): array
  414. {
  415. if (!empty($kpCodes)) {
  416. return $kpCodes;
  417. }
  418. // 如果没有指定知识点,返回一些通用的数学知识点
  419. return [
  420. '一元二次方程',
  421. '二次函数',
  422. '旋转',
  423. '圆',
  424. '概率初步',
  425. ];
  426. }
  427. /**
  428. * 获取学生薄弱点
  429. */
  430. private function getStudentWeaknesses(string $studentId, float $threshold): array
  431. {
  432. try {
  433. // 使用 StudentKnowledgeMastery 模型获取掌握度低于阈值的知识点
  434. $weaknessRecords = StudentKnowledgeMastery::forStudent($studentId)
  435. ->weaknesses($threshold)
  436. ->orderByMastery('asc')
  437. ->limit(20)
  438. ->with('knowledgePoint') // 预加载知识点信息
  439. ->get();
  440. // 转换为统一格式
  441. return $weaknessRecords->map(function ($record) {
  442. return [
  443. 'kp_code' => $record->kp_code,
  444. 'kp_name' => $record->knowledgePoint->name ?? $record->kp_code,
  445. 'mastery' => (float) ($record->mastery_level ?? 0),
  446. 'attempts' => (int) ($record->total_attempts ?? 0),
  447. 'correct' => (int) ($record->correct_attempts ?? 0),
  448. 'incorrect' => (int) ($record->incorrect_attempts ?? 0),
  449. 'confidence' => (float) ($record->confidence_level ?? 0),
  450. 'trend' => $record->mastery_trend ?? 'stable',
  451. ];
  452. })->toArray();
  453. } catch (\Exception $e) {
  454. Log::error('ExamTypeStrategy: 获取学生薄弱点失败', [
  455. 'student_id' => $studentId,
  456. 'threshold' => $threshold,
  457. 'error' => $e->getMessage()
  458. ]);
  459. return [];
  460. }
  461. }
  462. }