TaskManager.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Http\Client\Response;
  4. use Illuminate\Support\Facades\Cache;
  5. use Illuminate\Support\Facades\Http;
  6. use Illuminate\Support\Facades\Log;
  7. /**
  8. * 统一异步任务管理器
  9. * 负责所有异步任务的创建、状态管理、进度更新和回调通知
  10. */
  11. class TaskManager
  12. {
  13. /**
  14. * 任务类型常量
  15. */
  16. const TASK_TYPE_EXAM = 'exam'; // 智能出卷任务
  17. const TASK_TYPE_ANALYSIS = 'analysis'; // 学情分析任务
  18. /**
  19. * 任务状态常量
  20. */
  21. const STATUS_PENDING = 'pending';
  22. const STATUS_PROCESSING = 'processing';
  23. const STATUS_COMPLETED = 'completed';
  24. const STATUS_FAILED = 'failed';
  25. /**
  26. * 创建异步任务
  27. */
  28. public function createTask(string $type, array $data): string
  29. {
  30. $taskId = $this->generateTaskId($type, $data);
  31. $taskData = [
  32. 'task_id' => $taskId,
  33. 'type' => $type,
  34. 'status' => self::STATUS_PROCESSING,
  35. 'progress' => 0,
  36. 'message' => '任务已创建,正在处理...',
  37. 'data' => $data,
  38. 'created_at' => now()->toISOString(),
  39. 'updated_at' => now()->toISOString(),
  40. 'callback_url' => $data['callback_url'] ?? null,
  41. ];
  42. $this->saveTask($taskId, $taskData);
  43. // 保存任务映射(用于快速查找)
  44. $this->saveTaskMapping($type, $data, $taskId);
  45. Log::info('TaskManager: 任务已创建', [
  46. 'task_id' => $taskId,
  47. 'type' => $type,
  48. 'data_keys' => array_keys($data),
  49. ]);
  50. return $taskId;
  51. }
  52. /**
  53. * 获取任务状态
  54. */
  55. public function getTaskStatus(string $taskId): ?array
  56. {
  57. return $this->loadTask($taskId);
  58. }
  59. /**
  60. * 更新任务状态
  61. */
  62. public function updateTaskStatus(string $taskId, array $updates): void
  63. {
  64. $task = $this->loadTask($taskId);
  65. if (!$task) {
  66. Log::warning('TaskManager: 尝试更新不存在的任务', ['task_id' => $taskId]);
  67. return;
  68. }
  69. $updatedTask = array_merge($task, $updates, [
  70. 'updated_at' => now()->toISOString(),
  71. ]);
  72. $this->saveTask($taskId, $updatedTask);
  73. Log::info('TaskManager: 任务状态已更新', [
  74. 'task_id' => $taskId,
  75. 'status' => $updates['status'] ?? 'N/A',
  76. 'progress' => $updates['progress'] ?? 'N/A',
  77. ]);
  78. }
  79. /**
  80. * 更新任务进度
  81. */
  82. public function updateTaskProgress(string $taskId, int $progress, string $message): void
  83. {
  84. $this->updateTaskStatus($taskId, [
  85. 'progress' => $progress,
  86. 'message' => $message,
  87. ]);
  88. }
  89. /**
  90. * 标记任务完成
  91. */
  92. public function markTaskCompleted(string $taskId, array $result): void
  93. {
  94. $this->updateTaskStatus($taskId, array_merge($result, [
  95. 'status' => self::STATUS_COMPLETED,
  96. 'progress' => 100,
  97. 'message' => '任务已完成',
  98. 'completed_at' => now()->toISOString(),
  99. ]));
  100. }
  101. /**
  102. * 标记任务失败
  103. */
  104. public function markTaskFailed(string $taskId, string $error): void
  105. {
  106. $this->updateTaskStatus($taskId, [
  107. 'status' => self::STATUS_FAILED,
  108. 'progress' => 0,
  109. 'message' => '任务失败: ' . $error,
  110. 'error' => $error,
  111. ]);
  112. Log::error('TaskManager: 任务执行失败', [
  113. 'task_id' => $taskId,
  114. 'error' => $error,
  115. ]);
  116. }
  117. /**
  118. * 发送回调通知
  119. */
  120. public function sendCallback(string $taskId): void
  121. {
  122. $task = $this->loadTask($taskId);
  123. if (!$task || !$task['callback_url']) {
  124. return; // 没有回调URL或任务不存在
  125. }
  126. try {
  127. $payload = $this->buildCallbackPayload($task);
  128. $response = Http::timeout(30)->post($task['callback_url'], $payload);
  129. if ($response->successful()) {
  130. Log::info('TaskManager: 回调通知发送成功', [
  131. 'task_id' => $taskId,
  132. 'callback_url' => $task['callback_url'],
  133. ]);
  134. } else {
  135. Log::warning('TaskManager: 回调通知发送失败', [
  136. 'task_id' => $taskId,
  137. 'callback_url' => $task['callback_url'],
  138. 'status' => $response->status(),
  139. ]);
  140. }
  141. } catch (\Exception $e) {
  142. Log::error('TaskManager: 回调通知异常', [
  143. 'task_id' => $taskId,
  144. 'callback_url' => $task['callback_url'] ?? 'unknown',
  145. 'error' => $e->getMessage(),
  146. ]);
  147. }
  148. }
  149. /**
  150. * 生成任务ID
  151. */
  152. private function generateTaskId(string $type, array $data): string
  153. {
  154. $prefix = match ($type) {
  155. self::TASK_TYPE_EXAM => 'exam_task',
  156. self::TASK_TYPE_ANALYSIS => 'analysis_task',
  157. default => 'task_' . $type,
  158. };
  159. return $prefix . '_' . uniqid() . '_' . substr(md5(serialize($data) . time()), 0, 8);
  160. }
  161. /**
  162. * 保存任务到缓存
  163. */
  164. private function saveTask(string $taskId, array $taskData): void
  165. {
  166. Cache::put($this->getCacheKey($taskId), $taskData, now()->addDay());
  167. }
  168. /**
  169. * 从缓存加载任务
  170. */
  171. private function loadTask(string $taskId): ?array
  172. {
  173. return Cache::get($this->getCacheKey($taskId));
  174. }
  175. /**
  176. * 获取缓存键
  177. */
  178. private function getCacheKey(string $taskId): string
  179. {
  180. return "task:{$taskId}";
  181. }
  182. /**
  183. * 保存任务映射
  184. */
  185. private function saveTaskMapping(string $type, array $data, string $taskId): void
  186. {
  187. if ($type === self::TASK_TYPE_ANALYSIS) {
  188. $paperId = $data['paperId'] ?? $data['paper_id'] ?? null;
  189. $studentId = $data['studentId'] ?? $data['student_id'] ?? null;
  190. if ($paperId) {
  191. $mappingKey = $this->getTaskMappingKey($paperId, $studentId);
  192. Cache::put($mappingKey, $taskId, now()->addDay());
  193. }
  194. }
  195. }
  196. /**
  197. * 根据试卷ID查找分析任务
  198. */
  199. public function findAnalysisTaskByPaperId(string $paperId, ?string $studentId = null): ?array
  200. {
  201. // 尝试从映射缓存中获取任务ID
  202. $mappingKey = $this->getTaskMappingKey($paperId, $studentId);
  203. $taskId = Cache::get($mappingKey);
  204. if ($taskId) {
  205. $task = $this->loadTask($taskId);
  206. if ($task) {
  207. return $task;
  208. } else {
  209. // 任务不存在,清理映射缓存
  210. Cache::forget($mappingKey);
  211. }
  212. }
  213. return null;
  214. }
  215. /**
  216. * 获取任务映射缓存键
  217. */
  218. private function getTaskMappingKey(string $paperId, ?string $studentId = null): string
  219. {
  220. $studentPart = $studentId ? "_{$studentId}" : '';
  221. return "task_mapping:analysis:{$paperId}{$studentPart}";
  222. }
  223. /**
  224. * 构建回调负载
  225. */
  226. private function buildCallbackPayload(array $task): array
  227. {
  228. $basePayload = [
  229. 'task_id' => $task['task_id'],
  230. 'type' => $task['type'],
  231. 'status' => $task['status'],
  232. 'completed_at' => $task['completed_at'] ?? null,
  233. ];
  234. // 根据任务类型添加特定数据
  235. if ($task['type'] === self::TASK_TYPE_EXAM) {
  236. $basePayload['callback_type'] = 'exam_pdf_generated';
  237. $basePayload['paper_id'] = $task['data']['paper_id'] ?? null;
  238. $basePayload['pdfs'] = $task['pdfs'] ?? null;
  239. $basePayload['exam_content'] = $task['exam_content'] ?? null;
  240. } elseif ($task['type'] === self::TASK_TYPE_ANALYSIS) {
  241. $basePayload['callback_type'] = 'analysis_report_generated';
  242. $basePayload['paper_id'] = $task['data']['paper_id'] ?? $task['data']['paperId'] ?? null;
  243. $basePayload['student_id'] = $task['data']['student_id'] ?? $task['data']['studentId'] ?? null;
  244. $basePayload['pdf_url'] = $task['pdf_url'] ?? null;
  245. }
  246. return $basePayload;
  247. }
  248. }