ExamTypeStrategy.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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. default => $this->buildGeneralParams($baseParams)
  30. };
  31. }
  32. /**
  33. * 通用智能出卷(原有行为)
  34. */
  35. private function buildGeneralParams(array $params): array
  36. {
  37. Log::info('ExamTypeStrategy: 通用智能出卷参数', $params);
  38. // 返回原始参数,不做特殊处理
  39. return $params;
  40. }
  41. /**
  42. * 摸底测试:评估当前水平
  43. */
  44. private function buildDiagnosticParams(array $params): array
  45. {
  46. Log::info('ExamTypeStrategy: 构建摸底测试参数', $params);
  47. // 摸底测试:平衡所有难度,覆盖多个知识点
  48. $enhanced = array_merge($params, [
  49. // 难度配比:相对平衡,基础题稍多
  50. 'difficulty_ratio' => [
  51. '基础' => 40,
  52. '中等' => 40,
  53. '拔高' => 20,
  54. ],
  55. // 题型配比:选择题多一些,便于快速评估
  56. 'question_type_ratio' => [
  57. '选择题' => 50,
  58. '填空题' => 25,
  59. '解答题' => 25,
  60. ],
  61. // 确保覆盖多个知识点
  62. 'kp_codes' => $this->expandKpCodesForDiagnostic($params['kp_codes'] ?? []),
  63. // 摸底测试名称
  64. 'paper_name' => $params['paper_name'] ?? ('摸底测试_' . now()->format('Ymd_His')),
  65. ]);
  66. Log::info('ExamTypeStrategy: 摸底测试参数构建完成', [
  67. 'difficulty_ratio' => $enhanced['difficulty_ratio'],
  68. 'question_type_ratio' => $enhanced['question_type_ratio'],
  69. 'kp_codes_count' => count($enhanced['kp_codes'])
  70. ]);
  71. return $enhanced;
  72. }
  73. /**
  74. * 错题:针对薄弱点强化
  75. * 使用 QuestionExpansionService 按优先级扩展题目
  76. */
  77. private function buildMistakeParams(array $params): array
  78. {
  79. Log::info('ExamTypeStrategy: 构建错题参数', $params);
  80. $studentId = $params['student_id'] ?? null;
  81. $totalQuestions = $params['total_questions'] ?? 20;
  82. $mistakeOptions = $params['mistake_options'] ?? [];
  83. $weaknessThreshold = $mistakeOptions['weakness_threshold'] ?? 0.7;
  84. $intensity = $mistakeOptions['intensity'] ?? 'medium';
  85. $focusWeaknesses = $mistakeOptions['focus_weaknesses'] ?? true;
  86. // 根据强度调整难度配比
  87. $difficultyRatio = match($intensity) {
  88. 'low' => [
  89. '基础' => 60,
  90. '中等' => 35,
  91. '拔高' => 5,
  92. ],
  93. 'medium' => [
  94. '基础' => 45,
  95. '中等' => 40,
  96. '拔高' => 15,
  97. ],
  98. 'high' => [
  99. '基础' => 30,
  100. '中等' => 45,
  101. '拔高' => 25,
  102. ],
  103. default => [
  104. '基础' => 45,
  105. '中等' => 40,
  106. '拔高' => 15,
  107. ]
  108. };
  109. // 获取学生薄弱点
  110. $weaknessFilter = [];
  111. if ($studentId && $focusWeaknesses) {
  112. $weaknessFilter = $this->getStudentWeaknesses($studentId, $weaknessThreshold);
  113. Log::info('ExamTypeStrategy: 获取到学生薄弱点', [
  114. 'student_id' => $studentId,
  115. 'weakness_threshold' => $weaknessThreshold,
  116. 'weakness_count' => count($weaknessFilter)
  117. ]);
  118. }
  119. // 使用 QuestionExpansionService 按优先级扩展题目
  120. $questionStrategy = $this->questionExpansionService->expandQuestions(
  121. $params,
  122. $studentId,
  123. $weaknessFilter,
  124. $totalQuestions
  125. );
  126. // 获取错题知识点
  127. $mistakeKnowledgePoints = [];
  128. if ($studentId && !empty($questionStrategy['mistake_question_ids'])) {
  129. $mistakeRecords = MistakeRecord::forStudent($studentId)
  130. ->whereIn('question_id', $questionStrategy['mistake_question_ids'])
  131. ->select(['knowledge_point'])
  132. ->get();
  133. $mistakeKnowledgePoints = array_unique(array_filter($mistakeRecords->pluck('knowledge_point')->toArray()));
  134. Log::info('ExamTypeStrategy: 获取错题知识点', [
  135. 'knowledge_points' => $mistakeKnowledgePoints
  136. ]);
  137. }
  138. // 获取扩展统计
  139. $expansionStats = $this->questionExpansionService->getExpansionStats($questionStrategy);
  140. $enhanced = array_merge($params, [
  141. 'difficulty_ratio' => $difficultyRatio,
  142. 'mistake_ids' => $questionStrategy['mistake_ids'],
  143. 'mistake_question_ids' => $questionStrategy['mistake_question_ids'],
  144. // 错题回顾的知识点优先级
  145. 'priority_knowledge_points' => array_merge(
  146. array_values($mistakeKnowledgePoints), // 错题知识点优先
  147. array_column($weaknessFilter, 'kp_code') // 然后是薄弱点
  148. ),
  149. // 错题回顾更注重针对性
  150. 'question_type_ratio' => [
  151. '选择题' => 35,
  152. '填空题' => 30,
  153. '解答题' => 35,
  154. ],
  155. 'paper_name' => $params['paper_name'] ?? ('错题_' . now()->format('Ymd_His')),
  156. // 标记这是错题,用于后续处理
  157. 'is_mistake_exam' => true,
  158. 'weakness_filter' => $weaknessFilter,
  159. // 题目扩展统计
  160. 'question_expansion_stats' => $expansionStats
  161. ]);
  162. Log::info('ExamTypeStrategy: 错题参数构建完成', [
  163. 'intensity' => $intensity,
  164. 'total_questions_needed' => $totalQuestions,
  165. 'mistake_question_ids_count' => count($enhanced['mistake_question_ids']),
  166. 'priority_knowledge_points_count' => count($enhanced['priority_knowledge_points']),
  167. 'question_expansion_stats' => $enhanced['question_expansion_stats'],
  168. 'weakness_count' => count($weaknessFilter)
  169. ]);
  170. return $enhanced;
  171. }
  172. /**
  173. * 专项练习:针对特定技能或知识点练习
  174. */
  175. private function buildPracticeParams(array $params): array
  176. {
  177. Log::info('ExamTypeStrategy: 构建专项练习参数', $params);
  178. $studentId = $params['student_id'] ?? null;
  179. $practiceOptions = $params['practice_options'] ?? [];
  180. $weaknessThreshold = $practiceOptions['weakness_threshold'] ?? 0.7;
  181. $intensity = $practiceOptions['intensity'] ?? 'medium';
  182. $focusWeaknesses = $practiceOptions['focus_weaknesses'] ?? true;
  183. // 根据强度调整难度配比
  184. $difficultyRatio = match($intensity) {
  185. 'low' => [
  186. '基础' => 60,
  187. '中等' => 35,
  188. '拔高' => 5,
  189. ],
  190. 'medium' => [
  191. '基础' => 45,
  192. '中等' => 40,
  193. '拔高' => 15,
  194. ],
  195. 'high' => [
  196. '基础' => 30,
  197. '中等' => 45,
  198. '拔高' => 25,
  199. ],
  200. default => [
  201. '基础' => 45,
  202. '中等' => 40,
  203. '拔高' => 15,
  204. ]
  205. };
  206. // 获取学生薄弱点
  207. $weaknessFilter = [];
  208. if ($studentId && $focusWeaknesses) {
  209. $weaknessFilter = $this->getStudentWeaknesses($studentId, $weaknessThreshold);
  210. Log::info('ExamTypeStrategy: 获取到学生薄弱点', [
  211. 'student_id' => $studentId,
  212. 'weakness_threshold' => $weaknessThreshold,
  213. 'weakness_count' => count($weaknessFilter)
  214. ]);
  215. }
  216. // 优先使用薄弱点知识点,如果没有则使用用户选择的知识点
  217. $kpCodes = $params['kp_codes'] ?? [];
  218. if ($studentId && empty($kpCodes) && !empty($weaknessFilter)) {
  219. $kpCodes = array_column($weaknessFilter, 'kp_code');
  220. Log::info('ExamTypeStrategy: 使用薄弱点作为知识点', [
  221. 'kp_codes' => $kpCodes
  222. ]);
  223. }
  224. $enhanced = array_merge($params, [
  225. 'difficulty_ratio' => $difficultyRatio,
  226. 'kp_codes' => $kpCodes,
  227. // 专项练习更注重题型覆盖
  228. 'question_type_ratio' => [
  229. '选择题' => 40,
  230. '填空题' => 30,
  231. '解答题' => 30,
  232. ],
  233. 'paper_name' => $params['paper_name'] ?? ('专项练习_' . now()->format('Ymd_His')),
  234. // 标记这是专项练习,用于后续处理
  235. 'is_practice_exam' => true,
  236. 'weakness_filter' => $weaknessFilter,
  237. ]);
  238. Log::info('ExamTypeStrategy: 专项练习参数构建完成', [
  239. 'intensity' => $intensity,
  240. 'difficulty_ratio' => $enhanced['difficulty_ratio'],
  241. 'kp_codes_count' => count($enhanced['kp_codes']),
  242. 'weakness_count' => count($weaknessFilter)
  243. ]);
  244. return $enhanced;
  245. }
  246. /**
  247. * 教材同步:按教材章节出题
  248. */
  249. private function buildTextbookParams(array $params): array
  250. {
  251. Log::info('ExamTypeStrategy: 构建教材同步参数', $params);
  252. // 教材同步:按章节顺序,难度递增
  253. $textbookOptions = $params['textbook_options'] ?? [];
  254. $enhanced = array_merge($params, [
  255. // 教材同步:基础和中等题为主
  256. 'difficulty_ratio' => [
  257. '基础' => 50,
  258. '中等' => 40,
  259. '拔高' => 10,
  260. ],
  261. 'question_type_ratio' => [
  262. '选择题' => 40,
  263. '填空题' => 30,
  264. '解答题' => 30,
  265. ],
  266. 'paper_name' => $params['paper_name'] ?? ('教材同步_' . now()->format('Ymd_His')),
  267. 'textbook_options' => $textbookOptions,
  268. ]);
  269. return $enhanced;
  270. }
  271. /**
  272. * 知识点专练:单个或少量知识点深练
  273. */
  274. private function buildKnowledgeParams(array $params): array
  275. {
  276. Log::info('ExamTypeStrategy: 构建知识点专练参数', $params);
  277. // 知识点专练:深度挖掘,多角度考查
  278. $knowledgeOptions = $params['knowledge_options'] ?? [];
  279. $enhanced = array_merge($params, [
  280. // 知识点专练:难度分布更均匀
  281. 'difficulty_ratio' => [
  282. '基础' => 35,
  283. '中等' => 45,
  284. '拔高' => 20,
  285. ],
  286. 'question_type_ratio' => [
  287. '选择题' => 30,
  288. '填空题' => 35,
  289. '解答题' => 35,
  290. ],
  291. 'paper_name' => $params['paper_name'] ?? ('知识点专练_' . now()->format('Ymd_His')),
  292. 'knowledge_options' => $knowledgeOptions,
  293. ]);
  294. return $enhanced;
  295. }
  296. /**
  297. * 为摸底测试扩展知识点(确保覆盖全面)
  298. */
  299. private function expandKpCodesForDiagnostic(array $kpCodes): array
  300. {
  301. if (!empty($kpCodes)) {
  302. return $kpCodes;
  303. }
  304. // 如果没有指定知识点,返回一些通用的数学知识点
  305. return [
  306. '一元二次方程',
  307. '二次函数',
  308. '旋转',
  309. '圆',
  310. '概率初步',
  311. ];
  312. }
  313. /**
  314. * 获取学生薄弱点
  315. */
  316. private function getStudentWeaknesses(string $studentId, float $threshold): array
  317. {
  318. try {
  319. // 使用 StudentKnowledgeMastery 模型获取掌握度低于阈值的知识点
  320. $weaknessRecords = StudentKnowledgeMastery::forStudent($studentId)
  321. ->weaknesses($threshold)
  322. ->orderByMastery('asc')
  323. ->limit(20)
  324. ->with('knowledgePoint') // 预加载知识点信息
  325. ->get();
  326. // 转换为统一格式
  327. return $weaknessRecords->map(function ($record) {
  328. return [
  329. 'kp_code' => $record->kp_code,
  330. 'kp_name' => $record->knowledgePoint->name ?? $record->kp_code,
  331. 'mastery' => (float) ($record->mastery_level ?? 0),
  332. 'attempts' => (int) ($record->total_attempts ?? 0),
  333. 'correct' => (int) ($record->correct_attempts ?? 0),
  334. 'incorrect' => (int) ($record->incorrect_attempts ?? 0),
  335. 'confidence' => (float) ($record->confidence_level ?? 0),
  336. 'trend' => $record->mastery_trend ?? 'stable',
  337. ];
  338. })->toArray();
  339. } catch (\Exception $e) {
  340. Log::error('ExamTypeStrategy: 获取学生薄弱点失败', [
  341. 'student_id' => $studentId,
  342. 'threshold' => $threshold,
  343. 'error' => $e->getMessage()
  344. ]);
  345. return [];
  346. }
  347. }
  348. }