ExamPaperService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <?php
  2. namespace App\Services;
  3. use App\Models\OCRRecord;
  4. use App\Models\Paper;
  5. use App\Models\Student;
  6. use App\Models\Teacher;
  7. use Illuminate\Support\Facades\Log;
  8. class ExamPaperService
  9. {
  10. protected QuestionBankService $questionBankService;
  11. public function __construct(QuestionBankService $questionBankService)
  12. {
  13. $this->questionBankService = $questionBankService;
  14. }
  15. /**
  16. * 获取所有老师列表
  17. */
  18. public function getTeachers(?string $currentTeacherId = null): array
  19. {
  20. try {
  21. $query = Teacher::query()
  22. ->leftJoin('users as u', 'teachers.teacher_id', '=', 'u.user_id')
  23. ->select(
  24. 'teachers.teacher_id',
  25. 'teachers.name',
  26. 'teachers.subject',
  27. 'u.username',
  28. 'u.email'
  29. );
  30. // 如果指定了当前老师ID,只返回该老师
  31. if ($currentTeacherId) {
  32. $query->where('teachers.teacher_id', $currentTeacherId);
  33. }
  34. $teachers = $query->orderBy('teachers.name')->get();
  35. // 检查是否有学生没有对应的老师记录
  36. $teacherIds = $teachers->pluck('teacher_id')->toArray();
  37. $missingTeacherIds = Student::query()
  38. ->distinct()
  39. ->whereNotIn('teacher_id', $teacherIds)
  40. ->pluck('teacher_id')
  41. ->toArray();
  42. $teachersArray = $teachers->all();
  43. if (!empty($missingTeacherIds)) {
  44. foreach ($missingTeacherIds as $missingId) {
  45. $teachersArray[] = (object) [
  46. 'teacher_id' => $missingId,
  47. 'name' => '未知老师 (' . $missingId . ')',
  48. 'subject' => '未知',
  49. 'username' => null,
  50. 'email' => null
  51. ];
  52. }
  53. usort($teachersArray, function($a, $b) {
  54. return strcmp($a->name, $b->name);
  55. });
  56. }
  57. return $teachersArray;
  58. } catch (\Exception $e) {
  59. Log::error('ExamPaperService: 加载老师列表失败', [
  60. 'error' => $e->getMessage()
  61. ]);
  62. return [];
  63. }
  64. }
  65. /**
  66. * 获取指定老师的学生列表
  67. */
  68. public function getStudents(?string $teacherId): array
  69. {
  70. if (empty($teacherId)) {
  71. return [];
  72. }
  73. try {
  74. return Student::query()
  75. ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id')
  76. ->where('students.teacher_id', $teacherId)
  77. ->select(
  78. 'students.student_id',
  79. 'students.name',
  80. 'students.grade',
  81. 'students.class_name',
  82. 'u.username',
  83. 'u.email'
  84. )
  85. ->orderBy('students.grade')
  86. ->orderBy('students.class_name')
  87. ->orderBy('students.name')
  88. ->get()
  89. ->all();
  90. } catch (\Exception $e) {
  91. Log::error('ExamPaperService: 加载学生列表失败', [
  92. 'teacher_id' => $teacherId,
  93. 'error' => $e->getMessage()
  94. ]);
  95. return [];
  96. }
  97. }
  98. /**
  99. * 获取最近的记录(OCR和试卷)
  100. */
  101. public function getRecentRecords(?string $studentId = null, int $limit = 10): array
  102. {
  103. // 1. 获取OCR记录(图片上传)
  104. $ocrQuery = OCRRecord::with('student');
  105. if (!empty($studentId)) {
  106. $ocrQuery->where('student_id', $studentId);
  107. }
  108. $ocrRecords = $ocrQuery->latest()->take(5)->get()
  109. ->map(function($record) {
  110. $studentName = $record->student?->name ?: ('学生ID: ' . $record->student_id);
  111. return [
  112. 'type' => 'ocr_upload',
  113. 'id' => $record->id,
  114. 'record_id' => $record->id,
  115. 'paper_id' => null,
  116. 'student_id' => $record->student_id,
  117. 'student_name' => $studentName,
  118. 'paper_type' => $record->paper_type_label,
  119. 'paper_name' => $record->image_filename ?: '未命名图片',
  120. 'status' => $record->status,
  121. 'total_questions' => $record->total_questions,
  122. 'processed_questions' => $record->processed_questions ?? 0,
  123. 'created_at' => $record->created_at->format('Y-m-d H:i'),
  124. 'is_completed' => $record->status === 'completed',
  125. ];
  126. })->toArray();
  127. // 2. 获取所有Paper记录(包括草稿和已评分)
  128. $paperQuery = Paper::with('student');
  129. if (!empty($studentId)) {
  130. $paperQuery->where('student_id', $studentId);
  131. }
  132. $allPapers = $paperQuery->latest()->take(5)->get()
  133. ->map(function($paper) {
  134. $type = $paper->status === 'completed' ? 'graded_paper' : 'generated';
  135. $paperType = $paper->status === 'completed' ? '已评分试卷' : '系统生成试卷';
  136. $iconColor = $paper->status === 'completed' ? 'text-green-500' : 'text-blue-500';
  137. $studentName = $paper->student?->name ?: ('学生ID: ' . $paper->student_id);
  138. return [
  139. 'type' => $type,
  140. 'id' => $paper->paper_id,
  141. 'record_id' => null,
  142. 'paper_id' => $paper->paper_id,
  143. 'student_id' => $paper->student_id,
  144. 'student_name' => $studentName,
  145. 'paper_type' => $paperType,
  146. 'paper_name' => $paper->paper_name ?? '未命名试卷',
  147. 'status' => $paper->difficulty_category,
  148. 'total_questions' => $paper->question_count ?? 0,
  149. 'created_at' => $paper->created_at->format('Y-m-d H:i'),
  150. 'is_completed' => $paper->status === 'completed',
  151. 'icon_color' => $iconColor,
  152. ];
  153. })->toArray();
  154. // 3. 合并并按时间排序
  155. $allRecords = array_merge($ocrRecords, $allPapers);
  156. usort($allRecords, function($a, $b) {
  157. return strcmp($b['created_at'], $a['created_at']);
  158. });
  159. return array_slice($allRecords, 0, $limit);
  160. }
  161. /**
  162. * 获取学生的试卷列表
  163. */
  164. public function getStudentPapers(?string $studentId): array
  165. {
  166. if (empty($studentId)) {
  167. return [];
  168. }
  169. try {
  170. $student = Student::find($studentId);
  171. if (!$student) {
  172. Log::warning('ExamPaperService: 未找到指定学生', ['student_id' => $studentId]);
  173. return [];
  174. }
  175. return $student->papers()
  176. ->withCount('questions')
  177. ->orderBy('created_at', 'desc')
  178. ->take(20)
  179. ->get()
  180. ->map(function($paper) {
  181. return [
  182. 'paper_id' => $paper->paper_id,
  183. 'paper_name' => $paper->paper_name ?? '未命名试卷',
  184. 'total_questions' => $paper->questions_count ?? 0,
  185. 'total_score' => $paper->total_score ?? 0,
  186. 'created_at' => $paper->created_at->format('Y-m-d H:i'),
  187. ];
  188. })
  189. ->toArray();
  190. } catch (\Exception $e) {
  191. Log::error('ExamPaperService: 获取学生试卷列表失败', [
  192. 'student_id' => $studentId,
  193. 'error' => $e->getMessage()
  194. ]);
  195. return [];
  196. }
  197. }
  198. /**
  199. * 获取试卷题目详情
  200. */
  201. public function getPaperQuestions(?string $paperId): array
  202. {
  203. if (empty($paperId)) {
  204. return [];
  205. }
  206. try {
  207. // 首先检查试卷是否存在
  208. $paper = Paper::where('paper_id', $paperId)->first();
  209. if (!$paper) {
  210. Log::warning('ExamPaperService: 未找到指定试卷', ['paper_id' => $paperId]);
  211. return [];
  212. }
  213. // 使用关联关系查询题目 - 按题型和题号排序
  214. $paperWithQuestions = Paper::with(['questions' => function($query) {
  215. // 自定义排序:选择题、填空题、解答题的顺序
  216. $query->orderByRaw("
  217. CASE question_type
  218. WHEN 'choice' THEN 1
  219. WHEN 'fill' THEN 2
  220. WHEN 'answer' THEN 3
  221. ELSE 4
  222. END
  223. ")->orderBy('question_number');
  224. }])->where('paper_id', $paperId)->first();
  225. $questions = $paperWithQuestions ? $paperWithQuestions->questions : collect([]);
  226. // 处理数据不一致的情况:如果题目为空但试卷显示有题目
  227. if ($questions->isEmpty() && ($paper->question_count ?? 0) > 0) {
  228. Log::warning('ExamPaperService: 试卷显示有题目但实际题目数据缺失', [
  229. 'paper_id' => $paperId,
  230. 'expected_questions' => $paper->question_count,
  231. 'actual_questions' => 0
  232. ]);
  233. return [
  234. [
  235. 'id' => 'missing_data',
  236. 'question_number' => 1,
  237. 'question_bank_id' => null,
  238. 'question_type' => 'info',
  239. 'content' => "⚠️ 数据异常:试卷显示应有 {$paper->question_count} 道题目,但未找到题目数据。这通常是试卷创建过程中断导致的。请联系管理员或重新创建试卷。",
  240. 'answer' => '',
  241. 'score' => 0,
  242. 'is_missing_data' => true
  243. ]
  244. ];
  245. }
  246. if ($questions->isEmpty()) {
  247. Log::info('ExamPaperService: 试卷确实没有题目', ['paper_id' => $paperId]);
  248. return [
  249. [
  250. 'id' => 'no_questions',
  251. 'question_number' => 1,
  252. 'question_bank_id' => null,
  253. 'question_type' => 'info',
  254. 'content' => '该试卷暂无题目数据',
  255. 'answer' => '',
  256. 'score' => 0,
  257. 'is_empty' => true
  258. ]
  259. ];
  260. }
  261. // 获取题目详情
  262. $questionIds = $questions->pluck('question_bank_id')->filter()->unique()->toArray();
  263. if (empty($questionIds)) {
  264. Log::info('ExamPaperService: 题目没有关联题库ID', ['paper_id' => $paperId]);
  265. return $questions->map(function($q) {
  266. return [
  267. 'id' => $q->id,
  268. 'question_number' => $q->question_number,
  269. 'question_bank_id' => $q->question_bank_id,
  270. 'question_type' => $q->question_type,
  271. 'content' => '题目内容未关联到题库',
  272. 'answer' => '',
  273. 'score' => $q->score ?? 5,
  274. ];
  275. })->toArray();
  276. }
  277. $questionsResponse = $this->questionBankService->getQuestionsByIds($questionIds);
  278. $questionDetails = collect($questionsResponse['data'] ?? [])->keyBy('id');
  279. return $questions->map(function($q) use ($questionDetails) {
  280. $detail = $questionDetails->get($q->question_bank_id);
  281. // 获取题目内容,优先从 Question Bank 获取 stem,然后fallback
  282. $questionContent = null;
  283. if ($detail) {
  284. $questionContent = $detail['stem'] ?? $detail['content'] ?? $detail['question_text'] ?? null;
  285. }
  286. // 如果 Question Bank 中没有内容,尝试从本地数据库的 question_text 获取
  287. if (!$questionContent && $q->question_text) {
  288. $questionContent = $q->question_text;
  289. }
  290. // 如果还是没有内容,显示友好提示
  291. if (!$questionContent) {
  292. $questionContent = '⚠️ 题目内容暂未加载完整';
  293. }
  294. return [
  295. 'id' => $q->id,
  296. 'question_number' => $q->question_number,
  297. 'question_bank_id' => $q->question_bank_id,
  298. 'question_type' => $q->question_type,
  299. 'content' => $questionContent,
  300. 'answer' => $detail['answer'] ?? $detail['correct_answer'] ?? $q->answer ?? '',
  301. 'score' => $q->score ?? 5,
  302. 'kp_code' => $q->knowledge_point, // 从本地数据库获取知识点代码
  303. ];
  304. })->toArray();
  305. } catch (\Exception $e) {
  306. Log::error('ExamPaperService: 获取试卷题目失败', [
  307. 'paper_id' => $paperId,
  308. 'error' => $e->getMessage(),
  309. 'trace' => $e->getTraceAsString()
  310. ]);
  311. return [
  312. [
  313. 'id' => 'error',
  314. 'question_number' => 1,
  315. 'question_bank_id' => null,
  316. 'question_type' => 'error',
  317. 'content' => '获取题目数据时发生错误:' . $e->getMessage(),
  318. 'answer' => '',
  319. 'score' => 0,
  320. 'is_error' => true
  321. ]
  322. ];
  323. }
  324. }
  325. }