ExamAnswerAnalysisController.php 14 KB

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