MarkdownImport.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. namespace App\Models;
  3. use Illuminate\Database\Eloquent\Factories\HasFactory;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Database\Eloquent\Relations\HasMany;
  6. class MarkdownImport extends Model
  7. {
  8. use HasFactory;
  9. protected $fillable = [
  10. 'file_name',
  11. 'original_markdown',
  12. 'parsed_json',
  13. 'source_type',
  14. 'source_name',
  15. 'status',
  16. 'error_message',
  17. 'progress_stage',
  18. 'progress_message',
  19. 'progress_current',
  20. 'progress_total',
  21. 'progress_updated_at',
  22. 'processing_started_at',
  23. 'processing_finished_at',
  24. ];
  25. protected $casts = [
  26. 'created_at' => 'datetime',
  27. 'updated_at' => 'datetime',
  28. 'progress_updated_at' => 'datetime',
  29. 'processing_started_at' => 'datetime',
  30. 'processing_finished_at' => 'datetime',
  31. ];
  32. public const STATUS_PENDING = 'pending';
  33. public const STATUS_PARSED = 'parsed';
  34. public const STATUS_REVIEWED = 'reviewed';
  35. public const STATUS_COMPLETED = 'completed';
  36. public const STATUS_PROCESSING = 'processing';
  37. public const STATUS_FAILED = 'failed';
  38. public const STAGE_QUEUED = 'queued';
  39. public const STAGE_SPLITTING = 'splitting';
  40. public const STAGE_AI_PARSING = 'ai_parsing';
  41. public const STAGE_WRITING = 'writing';
  42. public const STAGE_PARSED = 'parsed';
  43. public const STAGE_COMPLETED = 'completed';
  44. public const STAGE_FAILED = 'failed';
  45. public function candidates(): HasMany
  46. {
  47. return $this->hasMany(PreQuestionCandidate::class, 'import_id');
  48. }
  49. public function preQuestions(): HasMany
  50. {
  51. return $this->hasMany(PreQuestion::class, 'import_id');
  52. }
  53. public function getStatusBadgeAttribute(): string
  54. {
  55. $badges = [
  56. self::STATUS_PENDING => 'gray',
  57. self::STATUS_PARSED => 'info',
  58. self::STATUS_REVIEWED => 'warning',
  59. self::STATUS_COMPLETED => 'success',
  60. ];
  61. return $badges[$this->status] ?? 'gray';
  62. }
  63. public function getProgressLabelAttribute(): string
  64. {
  65. $stageLabel = match ($this->progress_stage) {
  66. self::STAGE_QUEUED => '已排队',
  67. self::STAGE_SPLITTING => '拆题中',
  68. self::STAGE_AI_PARSING => 'AI 解析中',
  69. self::STAGE_WRITING => '写入候选库',
  70. self::STAGE_PARSED => '已解析',
  71. self::STAGE_COMPLETED => '已完成',
  72. self::STAGE_FAILED => '失败',
  73. default => $this->progress_stage ?: '—',
  74. };
  75. if (($this->progress_total ?? 0) > 0) {
  76. return sprintf(
  77. '%s %d/%d',
  78. $stageLabel,
  79. (int) ($this->progress_current ?? 0),
  80. (int) $this->progress_total
  81. );
  82. }
  83. return $stageLabel;
  84. }
  85. public function getProgressPercentAttribute(): ?int
  86. {
  87. $total = (int) ($this->progress_total ?? 0);
  88. if ($total <= 0) {
  89. return null;
  90. }
  91. $current = (int) ($this->progress_current ?? 0);
  92. return (int) max(0, min(100, round(($current / $total) * 100)));
  93. }
  94. public function getParsedCountAttribute(): int
  95. {
  96. if (array_key_exists('parsed_count', $this->attributes)) {
  97. return (int) $this->attributes['parsed_count'];
  98. }
  99. return $this->candidates()->count();
  100. }
  101. public function getAcceptedCountAttribute(): int
  102. {
  103. if (array_key_exists('accepted_count', $this->attributes)) {
  104. return (int) $this->attributes['accepted_count'];
  105. }
  106. return $this->candidates()->where('is_question_candidate', true)->count();
  107. }
  108. /**
  109. * 获取切分后的候选题目(新的切分格式)
  110. */
  111. public function getSplitCandidatesAttribute(): array
  112. {
  113. if (!$this->parsed_json) {
  114. return [];
  115. }
  116. $data = json_decode($this->parsed_json, true);
  117. return $data['candidates'] ?? [];
  118. }
  119. /**
  120. * 获取统计信息
  121. */
  122. public function getSplitStatisticsAttribute(): array
  123. {
  124. if (!$this->parsed_json) {
  125. return [];
  126. }
  127. $data = json_decode($this->parsed_json, true);
  128. return $data['statistics'] ?? [];
  129. }
  130. /**
  131. * 检查是否已完成
  132. */
  133. public function isCompleted(): bool
  134. {
  135. return $this->status === self::STATUS_COMPLETED;
  136. }
  137. /**
  138. * 检查是否正在处理
  139. */
  140. public function isProcessing(): bool
  141. {
  142. return $this->status === self::STATUS_PROCESSING;
  143. }
  144. /**
  145. * 检查是否失败
  146. */
  147. public function isFailed(): bool
  148. {
  149. return $this->status === self::STATUS_FAILED;
  150. }
  151. /**
  152. * 获取状态标签
  153. */
  154. public function getStatusLabelAttribute(): string
  155. {
  156. return match ($this->status) {
  157. self::STATUS_PENDING => '等待处理',
  158. self::STATUS_PROCESSING => '处理中',
  159. self::STATUS_COMPLETED => '已完成',
  160. self::STATUS_FAILED => '处理失败',
  161. self::STATUS_PARSED => '已解析',
  162. self::STATUS_REVIEWED => '已审核',
  163. default => '未知',
  164. };
  165. }
  166. /**
  167. * 获取状态颜色
  168. */
  169. public function getSplitStatusColorAttribute(): string
  170. {
  171. return match ($this->status) {
  172. self::STATUS_PENDING => 'gray',
  173. self::STATUS_PROCESSING => 'warning',
  174. self::STATUS_COMPLETED => 'success',
  175. self::STATUS_FAILED => 'danger',
  176. self::STATUS_PARSED => 'info',
  177. self::STATUS_REVIEWED => 'primary',
  178. default => 'gray',
  179. };
  180. }
  181. public function parseFilename(): array
  182. {
  183. if (empty($this->file_name)) {
  184. return [];
  185. }
  186. $base = pathinfo((string) $this->file_name, PATHINFO_FILENAME);
  187. $parts = array_map('trim', explode('_', $base));
  188. if (count($parts) < 4) {
  189. return [];
  190. }
  191. $series = $parts[0] ?? null;
  192. $grade = isset($parts[1]) && is_numeric($parts[1]) ? (int) $parts[1] : null;
  193. $termFlag = isset($parts[2]) && is_numeric($parts[2]) ? (int) $parts[2] : null;
  194. $subject = $parts[3] ?? null;
  195. $name = trim(implode('_', array_slice($parts, 4)));
  196. $term = match ($termFlag) {
  197. 1 => '上册',
  198. 2 => '下册',
  199. 0 => '上下册',
  200. default => null,
  201. };
  202. return [
  203. 'series' => $series,
  204. 'grade' => $grade,
  205. 'term' => $term,
  206. 'subject' => $subject,
  207. 'name' => $name !== '' ? $name : $base,
  208. ];
  209. }
  210. }