ExamAnswerAnalysisController.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Services\ExamAnswerAnalysisService;
  5. use Illuminate\Http\Request;
  6. use Illuminate\Http\JsonResponse;
  7. use Illuminate\Support\Facades\Log;
  8. use Illuminate\Support\Facades\Validator;
  9. /**
  10. * 考试答题分析 API 控制器
  11. * 支持步骤级分析的 RESTful API
  12. */
  13. class ExamAnswerAnalysisController extends Controller
  14. {
  15. public function __construct(
  16. private readonly ExamAnswerAnalysisService $analysisService
  17. ) {}
  18. /**
  19. * 分析考试答题数据
  20. *
  21. * POST /api/exam-answer-analysis
  22. *
  23. * @param Request $request
  24. * @return JsonResponse
  25. */
  26. public function analyze(Request $request): JsonResponse
  27. {
  28. // 临时增加执行时间限制(避免超时)
  29. set_time_limit(120);
  30. ini_set('max_execution_time', 120);
  31. try {
  32. // 验证请求数据
  33. $validator = Validator::make($request->all(), [
  34. 'paper_id' => 'required|string|max:255',
  35. 'student_id' => 'required|string|max:255',
  36. 'questions' => 'required|array|min:1',
  37. 'questions.*.question_bank_id' => 'required|string|max:255',
  38. 'questions.*.student_answer' => 'sometimes|string',
  39. 'questions.*.is_correct' => 'sometimes|array',
  40. 'questions.*.teacher_comment' => 'sometimes|string|nullable',
  41. 'questions.*.score' => 'sometimes|numeric|min:0',
  42. 'questions.*.score_obtained' => 'sometimes|numeric|min:0',
  43. 'questions.*.steps' => 'sometimes|array',
  44. 'questions.*.steps.*.step_index' => 'required_with:questions.*.steps|integer|min:1',
  45. 'questions.*.steps.*.is_correct' => 'required_with:questions.*.steps|boolean',
  46. 'questions.*.steps.*.kp_id' => 'required_with:questions.*.steps|string|max:255',
  47. 'questions.*.steps.*.score' => 'required_with:questions.*.steps|numeric|min:0',
  48. ]);
  49. if ($validator->fails()) {
  50. return response()->json([
  51. 'success' => false,
  52. 'error' => 'Validation failed',
  53. 'details' => $validator->errors()
  54. ], 422);
  55. }
  56. $examData = $request->only(['paper_id', 'student_id', 'questions']);
  57. // 【修改】将 question_bank_id 转换为 question_id 供内部使用
  58. if (isset($examData['questions'])) {
  59. foreach ($examData['questions'] as &$question) {
  60. if (isset($question['question_bank_id'])) {
  61. $question['question_id'] = $question['question_bank_id'];
  62. unset($question['question_bank_id']);
  63. }
  64. }
  65. }
  66. // 调用分析服务
  67. $result = $this->analysisService->analyzeExamAnswers($examData);
  68. return response()->json([
  69. 'success' => true,
  70. 'data' => $result,
  71. 'message' => '分析完成'
  72. ]);
  73. } catch (\Exception $e) {
  74. Log::error('考试答题分析失败', [
  75. 'error' => $e->getMessage(),
  76. 'trace' => $e->getTraceAsString(),
  77. 'request_data' => $request->all()
  78. ]);
  79. return response()->json([
  80. 'success' => false,
  81. 'error' => '分析失败:' . $e->getMessage()
  82. ], 500);
  83. }
  84. }
  85. /**
  86. * 获取分析结果
  87. *
  88. * GET /api/exam-answer-analysis/{student_id}/{paper_id}
  89. *
  90. * @param string $studentId
  91. * @param string $paperId
  92. * @return JsonResponse
  93. */
  94. public function getAnalysisResult(string $studentId, string $paperId): JsonResponse
  95. {
  96. try {
  97. $result = \DB::connection('mysql')
  98. ->table('exam_analysis_results')
  99. ->where('student_id', $studentId)
  100. ->where('paper_id', $paperId)
  101. ->orderBy('created_at', 'desc')
  102. ->first();
  103. if (!$result) {
  104. return response()->json([
  105. 'success' => false,
  106. 'error' => '未找到分析结果'
  107. ], 404);
  108. }
  109. return response()->json([
  110. 'success' => true,
  111. 'data' => json_decode($result->analysis_data, true)
  112. ]);
  113. } catch (\Exception $e) {
  114. Log::error('获取分析结果失败', [
  115. 'student_id' => $studentId,
  116. 'paper_id' => $paperId,
  117. 'error' => $e->getMessage()
  118. ]);
  119. return response()->json([
  120. 'success' => false,
  121. 'error' => '获取分析结果失败:' . $e->getMessage()
  122. ], 500);
  123. }
  124. }
  125. /**
  126. * 获取学生历史分析记录
  127. *
  128. * GET /api/exam-answer-analysis/history/{student_id}
  129. *
  130. * @param string $studentId
  131. * @param Request $request
  132. * @return JsonResponse
  133. */
  134. public function getHistory(string $studentId, Request $request): JsonResponse
  135. {
  136. try {
  137. $limit = $request->input('limit', 10);
  138. $offset = $request->input('offset', 0);
  139. $results = \DB::connection('mysql')
  140. ->table('exam_analysis_results')
  141. ->where('student_id', $studentId)
  142. ->orderBy('created_at', 'desc')
  143. ->offset($offset)
  144. ->limit($limit)
  145. ->get()
  146. ->map(function ($item) {
  147. return [
  148. 'paper_id' => $item->paper_id,
  149. 'created_at' => $item->created_at,
  150. 'analysis_summary' => json_decode($item->analysis_data, true)['overall_summary'] ?? null
  151. ];
  152. });
  153. return response()->json([
  154. 'success' => true,
  155. 'data' => $results,
  156. 'pagination' => [
  157. 'limit' => $limit,
  158. 'offset' => $offset,
  159. 'total' => \DB::connection('mysql')
  160. ->table('exam_analysis_results')
  161. ->where('student_id', $studentId)
  162. ->count()
  163. ]
  164. ]);
  165. } catch (\Exception $e) {
  166. Log::error('获取历史分析记录失败', [
  167. 'student_id' => $studentId,
  168. 'error' => $e->getMessage()
  169. ]);
  170. return response()->json([
  171. 'success' => false,
  172. 'error' => '获取历史记录失败:' . $e->getMessage()
  173. ], 500);
  174. }
  175. }
  176. /**
  177. * 获取知识点掌握度趋势
  178. *
  179. * GET /api/exam-answer-analysis/mastery-trend/{student_id}
  180. *
  181. * @param string $studentId
  182. * @param Request $request
  183. * @return JsonResponse
  184. */
  185. public function getMasteryTrend(string $studentId, Request $request): JsonResponse
  186. {
  187. try {
  188. $kpCode = $request->input('kp_code');
  189. $query = \DB::connection('mysql')
  190. ->table('exam_analysis_results')
  191. ->where('student_id', $studentId)
  192. ->orderBy('created_at', 'desc');
  193. if ($kpCode) {
  194. $query->whereRaw("analysis_data->>'kp_code' = ?", [$kpCode]);
  195. }
  196. $results = $query->limit(20)->get();
  197. $trendData = [];
  198. foreach ($results as $result) {
  199. $analysisData = json_decode($result->analysis_data, true);
  200. if (isset($analysisData['mastery_vector'])) {
  201. foreach ($analysisData['mastery_vector'] as $kpId => $data) {
  202. if (!$kpCode || $kpId === $kpCode) {
  203. $trendData[$kpId][] = [
  204. 'date' => $result->created_at,
  205. 'mastery' => $data['current_mastery'],
  206. 'change' => $data['change'] ?? 0
  207. ];
  208. }
  209. }
  210. }
  211. }
  212. return response()->json([
  213. 'success' => true,
  214. 'data' => $trendData
  215. ]);
  216. } catch (\Exception $e) {
  217. Log::error('获取掌握度趋势失败', [
  218. 'student_id' => $studentId,
  219. 'error' => $e->getMessage()
  220. ]);
  221. return response()->json([
  222. 'success' => false,
  223. 'error' => '获取趋势数据失败:' . $e->getMessage()
  224. ], 500);
  225. }
  226. }
  227. /**
  228. * 获取智能出卷推荐
  229. *
  230. * GET /api/exam-answer-analysis/smart-quiz/{student_id}
  231. *
  232. * @param string $studentId
  233. * @param Request $request
  234. * @return JsonResponse
  235. */
  236. public function getSmartQuizRecommendation(string $studentId, Request $request): JsonResponse
  237. {
  238. try {
  239. // 获取最近一次分析结果
  240. $latestAnalysis = \DB::connection('mysql')
  241. ->table('exam_analysis_results')
  242. ->where('student_id', $studentId)
  243. ->orderBy('created_at', 'desc')
  244. ->first();
  245. if (!$latestAnalysis) {
  246. return response()->json([
  247. 'success' => false,
  248. 'error' => '未找到分析记录,无法生成推荐'
  249. ], 404);
  250. }
  251. $analysisData = json_decode($latestAnalysis->analysis_data, true);
  252. $recommendation = $analysisData['smart_quiz_recommendation'] ?? null;
  253. if (!$recommendation) {
  254. return response()->json([
  255. 'success' => false,
  256. 'error' => '未找到推荐数据'
  257. ], 404);
  258. }
  259. return response()->json([
  260. 'success' => true,
  261. 'data' => $recommendation,
  262. 'based_on_exam' => $latestAnalysis->paper_id,
  263. 'generated_at' => $latestAnalysis->created_at
  264. ]);
  265. } catch (\Exception $e) {
  266. Log::error('获取智能出卷推荐失败', [
  267. 'student_id' => $studentId,
  268. 'error' => $e->getMessage()
  269. ]);
  270. return response()->json([
  271. 'success' => false,
  272. 'error' => '获取推荐失败:' . $e->getMessage()
  273. ], 500);
  274. }
  275. }
  276. /**
  277. * 导出分析报告
  278. *
  279. * GET /api/exam-answer-analysis/export/{student_id}/{paper_id}
  280. *
  281. * @param string $studentId
  282. * @param string $paperId
  283. * @param Request $request
  284. * @return JsonResponse
  285. */
  286. public function export(string $studentId, string $paperId, Request $request): JsonResponse
  287. {
  288. try {
  289. $format = $request->input('format', 'json'); // json, pdf
  290. $result = \DB::connection('mysql')
  291. ->table('exam_analysis_results')
  292. ->where('student_id', $studentId)
  293. ->where('paper_id', $paperId)
  294. ->orderBy('created_at', 'desc')
  295. ->first();
  296. if (!$result) {
  297. return response()->json([
  298. 'success' => false,
  299. 'error' => '未找到分析结果'
  300. ], 404);
  301. }
  302. $analysisData = json_decode($result->analysis_data, true);
  303. if ($format === 'pdf') {
  304. // TODO: 实现 PDF 导出
  305. return response()->json([
  306. 'success' => false,
  307. 'error' => 'PDF 导出功能尚未实现'
  308. ], 501);
  309. }
  310. return response()->json([
  311. 'success' => true,
  312. 'data' => $analysisData,
  313. 'metadata' => [
  314. 'student_id' => $studentId,
  315. 'paper_id' => $paperId,
  316. 'generated_at' => $result->created_at,
  317. 'format' => $format
  318. ]
  319. ]);
  320. } catch (\Exception $e) {
  321. Log::error('导出分析报告失败', [
  322. 'student_id' => $studentId,
  323. 'paper_id' => $paperId,
  324. 'error' => $e->getMessage()
  325. ]);
  326. return response()->json([
  327. 'success' => false,
  328. 'error' => '导出失败:' . $e->getMessage()
  329. ], 500);
  330. }
  331. }
  332. /**
  333. * 批量分析多个学生的考试数据
  334. *
  335. * POST /api/exam-answer-analysis/batch
  336. *
  337. * @param Request $request
  338. * @return JsonResponse
  339. */
  340. public function batchAnalyze(Request $request): JsonResponse
  341. {
  342. try {
  343. $validator = Validator::make($request->all(), [
  344. 'exam_data_list' => 'required|array|min:1',
  345. 'exam_data_list.*.paper_id' => 'required|string',
  346. 'exam_data_list.*.student_id' => 'required|string',
  347. 'exam_data_list.*.questions' => 'required|array',
  348. ]);
  349. if ($validator->fails()) {
  350. return response()->json([
  351. 'success' => false,
  352. 'error' => 'Validation failed',
  353. 'details' => $validator->errors()
  354. ], 422);
  355. }
  356. $examDataList = $request->input('exam_data_list');
  357. $results = [];
  358. foreach ($examDataList as $examData) {
  359. try {
  360. $result = $this->analysisService->analyzeExamAnswers($examData);
  361. $results[] = [
  362. 'student_id' => $examData['student_id'],
  363. 'paper_id' => $examData['paper_id'],
  364. 'success' => true,
  365. 'data' => $result
  366. ];
  367. } catch (\Exception $e) {
  368. $results[] = [
  369. 'student_id' => $examData['student_id'],
  370. 'paper_id' => $examData['paper_id'],
  371. 'success' => false,
  372. 'error' => $e->getMessage()
  373. ];
  374. }
  375. }
  376. $successCount = count(array_filter($results, fn($r) => $r['success']));
  377. return response()->json([
  378. 'success' => true,
  379. 'data' => $results,
  380. 'summary' => [
  381. 'total' => count($results),
  382. 'success' => $successCount,
  383. 'failed' => count($results) - $successCount
  384. ],
  385. 'message' => "批量分析完成,成功 {$successCount} 条,失败 " . (count($results) - $successCount) . " 条"
  386. ]);
  387. } catch (\Exception $e) {
  388. Log::error('批量分析失败', [
  389. 'error' => $e->getMessage(),
  390. 'request_data' => $request->all()
  391. ]);
  392. return response()->json([
  393. 'success' => false,
  394. 'error' => '批量分析失败:' . $e->getMessage()
  395. ], 500);
  396. }
  397. }
  398. }