ExamAnswerAnalysisController.php 14 KB

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