ExamAnswerAnalysisController.php 15 KB

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