ExamAnswerAnalysisController.php 15 KB

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