PaperPayloadService.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Paper;
  4. use App\Models\PaperQuestion;
  5. use Illuminate\Support\Facades\Log;
  6. class PaperPayloadService
  7. {
  8. public function buildExamContent(Paper $paper): array
  9. {
  10. $paper->loadMissing('questions');
  11. $questions = $paper->questions ?? collect();
  12. $codes = $this->generatePaperCodes($paper->paper_id);
  13. return [
  14. 'paper_info' => [
  15. 'paper_id' => $paper->paper_id,
  16. 'paper_name' => $paper->paper_name ?? '',
  17. 'student_id' => $paper->student_id ?? '',
  18. 'teacher_id' => $paper->teacher_id ?? '',
  19. 'total_questions' => $questions->count(),
  20. 'total_score' => $paper->total_score ?? 0,
  21. 'difficulty_category' => $paper->difficulty_category ?? '基础',
  22. 'created_at' => $paper->created_at?->toISOString(),
  23. 'updated_at' => $paper->updated_at?->toISOString(),
  24. 'exam_code' => $codes['exam_code'],
  25. 'grading_code' => $codes['grading_code'],
  26. 'paper_id_num' => $codes['paper_id_num'],
  27. ],
  28. 'questions' => $questions->map(function (PaperQuestion $q) {
  29. $options = [];
  30. if ($q->question_type === 'choice') {
  31. $questionText = $q->question_text ?? '';
  32. preg_match_all('/([A-D])\s*[\.\、\:]\s*([^A-D]+?)(?=[A-D]\s*[\.\、\:]|$)/u', $questionText, $matches, PREG_SET_ORDER);
  33. foreach ($matches as $match) {
  34. $options[] = [
  35. 'label' => $match[1],
  36. 'content' => trim($match[2]),
  37. ];
  38. }
  39. }
  40. return [
  41. 'question_number' => $q->question_number,
  42. 'question_id' => $q->question_id,
  43. 'question_bank_id' => $q->question_bank_id,
  44. 'question_type' => $q->question_type,
  45. 'knowledge_point' => $q->knowledge_point,
  46. 'difficulty' => $q->difficulty,
  47. 'score' => $q->score,
  48. 'estimated_time' => $q->estimated_time,
  49. 'stem' => $q->question_text ?? '',
  50. 'options' => $options,
  51. 'correct_answer' => $q->correct_answer ?? '',
  52. 'solution' => $q->solution ?? '',
  53. 'student_answer' => $q->student_answer,
  54. 'is_correct' => $q->is_correct,
  55. 'score_obtained' => $q->score_obtained,
  56. 'score_ratio' => $q->score_ratio,
  57. 'teacher_comment' => $q->teacher_comment,
  58. 'graded_at' => $q->graded_at?->toISOString(),
  59. 'graded_by' => $q->graded_by,
  60. 'metadata' => [
  61. 'has_solution' => !empty($q->solution),
  62. 'is_choice' => $q->question_type === 'choice',
  63. 'is_fill' => $q->question_type === 'fill',
  64. 'is_answer' => $q->question_type === 'answer',
  65. 'difficulty_label' => $this->getDifficultyLabel($q->difficulty),
  66. 'question_type_label' => $this->getQuestionTypeLabel($q->question_type),
  67. ],
  68. ];
  69. })->toArray(),
  70. 'statistics' => [
  71. 'type_distribution' => $this->getTypeDistribution($questions),
  72. 'difficulty_distribution' => $this->getDifficultyDistribution($questions),
  73. 'knowledge_point_distribution' => $this->getKnowledgePointDistribution($questions),
  74. 'average_difficulty' => $questions->avg('difficulty'),
  75. 'total_estimated_time' => $questions->sum('estimated_time'),
  76. ],
  77. 'knowledge_points' => $questions->pluck('knowledge_point')->unique()->filter()->values()->toArray(),
  78. 'skills' => $this->extractSkillsFromQuestions($questions),
  79. ];
  80. }
  81. public function buildPaperApiPayload(Paper $paper): array
  82. {
  83. $examContent = $this->buildExamContent($paper);
  84. $codes = $this->generatePaperCodes($paper->paper_id);
  85. return [
  86. 'success' => true,
  87. 'paper_id' => $paper->paper_id,
  88. 'status' => 'ready',
  89. 'exam_code' => $codes['exam_code'],
  90. 'grading_code' => $codes['grading_code'],
  91. 'paper_id_num' => $codes['paper_id_num'],
  92. 'exam_content' => $examContent,
  93. 'urls' => [
  94. 'grading_url' => route('filament.admin.auth.intelligent-exam.grading', ['paper_id' => $paper->paper_id]),
  95. 'student_exam_url' => route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $paper->paper_id, 'answer' => 'false']),
  96. ],
  97. 'pdfs' => [
  98. 'exam_paper_pdf' => $paper->exam_pdf_url,
  99. 'grading_pdf' => $paper->grading_pdf_url,
  100. ],
  101. 'stats' => $examContent['statistics'] ?? null,
  102. 'created_at' => $paper->created_at?->toISOString(),
  103. 'updated_at' => $paper->updated_at?->toISOString(),
  104. ];
  105. }
  106. public function generatePaperCodes(string $paperId): array
  107. {
  108. // 提取15位数字ID作为统一学案编号
  109. preg_match('/paper_(\d{15})/', $paperId, $matches);
  110. $paperIdNum = $matches[1] ?? '';
  111. if ($paperIdNum === '') {
  112. $digits = preg_replace('/[^0-9]/', '', $paperId);
  113. $paperIdNum = substr(str_pad($digits, 15, '0', STR_PAD_LEFT), -15);
  114. Log::warning('PaperPayloadService: paper_id 不符合标准格式,已执行15位归一化', [
  115. 'paper_id' => $paperId,
  116. 'normalized_paper_id_num' => $paperIdNum,
  117. ]);
  118. }
  119. return [
  120. 'paper_id_num' => $paperIdNum,
  121. 'exam_code' => $paperIdNum,
  122. 'grading_code' => $paperIdNum,
  123. ];
  124. }
  125. private function getQuestionTypeLabel(?string $type): string
  126. {
  127. return match ($type) {
  128. 'choice' => '选择题',
  129. 'fill' => '填空题',
  130. 'answer' => '解答题',
  131. default => '未知题型',
  132. };
  133. }
  134. private function getDifficultyLabel(?float $difficulty): string
  135. {
  136. if ($difficulty === null) {
  137. return '未知';
  138. }
  139. if ($difficulty <= 0.4) {
  140. return '基础';
  141. }
  142. if ($difficulty <= 0.7) {
  143. return '中等';
  144. }
  145. return '拔高';
  146. }
  147. private function getTypeDistribution($questions): array
  148. {
  149. $distribution = [];
  150. foreach ($questions as $question) {
  151. $type = $question->question_type;
  152. $distribution[$type] = ($distribution[$type] ?? 0) + 1;
  153. }
  154. return $distribution;
  155. }
  156. private function getDifficultyDistribution($questions): array
  157. {
  158. $distribution = [];
  159. foreach ($questions as $question) {
  160. $label = $this->getDifficultyLabel($question->difficulty);
  161. $distribution[$label] = ($distribution[$label] ?? 0) + 1;
  162. }
  163. return $distribution;
  164. }
  165. private function getKnowledgePointDistribution($questions): array
  166. {
  167. $distribution = [];
  168. foreach ($questions as $question) {
  169. $kp = $question->knowledge_point;
  170. if ($kp) {
  171. $distribution[$kp] = ($distribution[$kp] ?? 0) + 1;
  172. }
  173. }
  174. return $distribution;
  175. }
  176. private function extractSkillsFromQuestions($questions): array
  177. {
  178. $skills = [];
  179. foreach ($questions as $question) {
  180. $solution = $question->solution ?? '';
  181. if ($solution) {
  182. $skillKeywords = ['代入法', '配方法', '因式分解', '换元法', '判别式', '求根公式', '韦达定理'];
  183. foreach ($skillKeywords as $keyword) {
  184. if (strpos($solution, $keyword) !== false) {
  185. $skills[] = $keyword;
  186. }
  187. }
  188. }
  189. $stem = $question->question_text ?? '';
  190. if ($stem) {
  191. preg_match_all('/\{([^}]+)\}/', $stem, $matches);
  192. foreach ($matches[1] as $match) {
  193. $skillList = array_map('trim', explode(',', $match));
  194. $skills = array_merge($skills, $skillList);
  195. }
  196. }
  197. }
  198. return array_unique(array_filter($skills));
  199. }
  200. }