ExamAnalysisApiController.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Models\Paper;
  5. use App\Services\ExamPdfExportService;
  6. use Illuminate\Http\JsonResponse;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Support\Facades\Http;
  9. use Illuminate\Support\Facades\Log;
  10. use Illuminate\Support\Facades\URL;
  11. class ExamAnalysisApiController extends Controller
  12. {
  13. /**
  14. * 生成学情报告(异步模式)
  15. * 立即返回任务ID,PDF生成在后台进行
  16. */
  17. public function store(Request $request, ExamPdfExportService $pdfExportService): JsonResponse
  18. {
  19. $data = $request->validate([
  20. 'paper_id' => 'required|string',
  21. 'student_id' => 'nullable|string',
  22. 'callback_url' => 'nullable|url',
  23. ]);
  24. $paperId = $data['paper_id'];
  25. $studentId = $data['student_id'] ?? null;
  26. $paper = Paper::find($paperId);
  27. if (!$paper) {
  28. return response()->json([
  29. 'success' => false,
  30. 'message' => '未找到试卷',
  31. ], 404);
  32. }
  33. if (!$studentId) {
  34. $studentId = $paper->student_id;
  35. }
  36. if (!$studentId) {
  37. return response()->json([
  38. 'success' => false,
  39. 'message' => '缺少 student_id',
  40. ], 422);
  41. }
  42. try {
  43. // 创建异步任务
  44. $taskId = $this->createAsyncTask($paperId, $studentId, $data);
  45. // 立即返回任务信息
  46. $viewUrl = URL::to("/admin/exam-analysis?paperId={$paperId}&studentId={$studentId}");
  47. $payload = [
  48. 'success' => true,
  49. 'message' => '学情报告任务已创建,正在后台生成PDF...',
  50. 'data' => [
  51. 'task_id' => $taskId,
  52. 'paper_id' => $paperId,
  53. 'student_id' => $studentId,
  54. 'status' => 'processing',
  55. 'analysis_url' => $viewUrl,
  56. 'pdf_url' => null, // 稍后生成
  57. 'created_at' => now()->toISOString(),
  58. ],
  59. ];
  60. return response()->json($payload, 200, [], JSON_UNESCAPED_SLASHES);
  61. } catch (\Exception $e) {
  62. Log::error('学情报告API失败', [
  63. 'paper_id' => $paperId,
  64. 'student_id' => $studentId,
  65. 'error' => $e->getMessage(),
  66. ]);
  67. return response()->json([
  68. 'success' => false,
  69. 'message' => '服务异常,请稍后重试',
  70. ], 500);
  71. }
  72. }
  73. /**
  74. * 轮询任务状态
  75. */
  76. public function status(string $taskId): JsonResponse
  77. {
  78. try {
  79. $task = $this->getTaskStatus($taskId);
  80. if (!$task) {
  81. return response()->json([
  82. 'success' => false,
  83. 'message' => '任务不存在',
  84. ], 404);
  85. }
  86. return response()->json([
  87. 'success' => true,
  88. 'data' => $task,
  89. ]);
  90. } catch (\Exception $e) {
  91. Log::error('查询学情报告任务状态失败', [
  92. 'task_id' => $taskId,
  93. 'error' => $e->getMessage(),
  94. ]);
  95. return response()->json([
  96. 'success' => false,
  97. 'message' => '查询失败,请稍后重试',
  98. ], 500);
  99. }
  100. }
  101. /**
  102. * 创建异步任务
  103. */
  104. private function createAsyncTask(string $paperId, string $studentId, array $data): string
  105. {
  106. $taskId = 'analysis_' . uniqid() . '_' . substr(md5($paperId . $studentId . time()), 0, 8);
  107. // 保存任务信息到缓存
  108. $taskData = [
  109. 'task_id' => $taskId,
  110. 'paper_id' => $paperId,
  111. 'student_id' => $studentId,
  112. 'status' => 'processing',
  113. 'created_at' => now()->toISOString(),
  114. 'updated_at' => now()->toISOString(),
  115. 'progress' => 0,
  116. 'message' => '正在生成学情报告...',
  117. 'data' => $data,
  118. 'callback_url' => $data['callback_url'] ?? null,
  119. ];
  120. // 保存到缓存,24小时过期
  121. cache()->put("analysis_task:{$taskId}", $taskData, now()->addDay());
  122. // 触发后台处理
  123. $this->processAnalysisGeneration($taskId, $paperId, $studentId);
  124. return $taskId;
  125. }
  126. /**
  127. * 获取任务状态
  128. */
  129. private function getTaskStatus(string $taskId): ?array
  130. {
  131. return cache()->get("analysis_task:{$taskId}");
  132. }
  133. /**
  134. * 处理学情报告生成
  135. */
  136. private function processAnalysisGeneration(string $taskId, string $paperId, string $studentId): void
  137. {
  138. try {
  139. // 更新任务状态
  140. $this->updateTaskStatus($taskId, [
  141. 'status' => 'processing',
  142. 'progress' => 10,
  143. 'message' => '开始生成学情报告...',
  144. ]);
  145. // 生成学情报告PDF
  146. $pdfUrl = app(ExamPdfExportService::class)->generateAnalysisReportPdf($paperId, $studentId);
  147. // 更新任务状态为完成
  148. $this->updateTaskStatus($taskId, [
  149. 'status' => 'completed',
  150. 'progress' => 100,
  151. 'message' => '学情报告生成完成',
  152. 'pdf_url' => $pdfUrl,
  153. 'completed_at' => now()->toISOString(),
  154. ]);
  155. Log::info('学情报告异步任务完成', [
  156. 'task_id' => $taskId,
  157. 'paper_id' => $paperId,
  158. 'student_id' => $studentId,
  159. 'pdf_url' => $pdfUrl,
  160. ]);
  161. // 发送回调通知
  162. $this->sendCallbackNotification($taskId);
  163. } catch (\Exception $e) {
  164. Log::error('学情报告生成失败', [
  165. 'task_id' => $taskId,
  166. 'paper_id' => $paperId,
  167. 'student_id' => $studentId,
  168. 'error' => $e->getMessage(),
  169. ]);
  170. // 更新任务状态为失败
  171. $this->updateTaskStatus($taskId, [
  172. 'status' => 'failed',
  173. 'progress' => 0,
  174. 'message' => '学情报告生成失败: ' . $e->getMessage(),
  175. 'error' => $e->getMessage(),
  176. ]);
  177. }
  178. }
  179. /**
  180. * 更新任务状态
  181. */
  182. private function updateTaskStatus(string $taskId, array $updates): void
  183. {
  184. $task = $this->getTaskStatus($taskId);
  185. if (!$task) {
  186. return;
  187. }
  188. $updatedTask = array_merge($task, $updates, [
  189. 'updated_at' => now()->toISOString(),
  190. ]);
  191. cache()->put("analysis_task:{$taskId}", $updatedTask, now()->addDay());
  192. }
  193. /**
  194. * 发送回调通知
  195. */
  196. private function sendCallbackNotification(string $taskId): void
  197. {
  198. $task = $this->getTaskStatus($taskId);
  199. if (!$task || !$task['callback_url']) {
  200. return;
  201. }
  202. try {
  203. $payload = [
  204. 'task_id' => $task['task_id'],
  205. 'paper_id' => $task['paper_id'],
  206. 'student_id' => $task['student_id'],
  207. 'status' => $task['status'],
  208. 'pdf_url' => $task['pdf_url'] ?? null,
  209. 'completed_at' => $task['completed_at'],
  210. 'callback_type' => 'analysis_report_generated',
  211. ];
  212. $response = Http::timeout(30)
  213. ->post($task['callback_url'], $payload);
  214. if ($response->successful()) {
  215. Log::info('学情报告回调通知发送成功', [
  216. 'task_id' => $taskId,
  217. 'callback_url' => $task['callback_url'],
  218. ]);
  219. } else {
  220. Log::warning('学情报告回调通知发送失败', [
  221. 'task_id' => $taskId,
  222. 'callback_url' => $task['callback_url'],
  223. 'status' => $response->status(),
  224. ]);
  225. }
  226. } catch (\Exception $e) {
  227. Log::error('学情报告回调通知异常', [
  228. 'task_id' => $taskId,
  229. 'callback_url' => $task['callback_url'] ?? 'unknown',
  230. 'error' => $e->getMessage(),
  231. ]);
  232. }
  233. }
  234. }