'datetime', 'updated_at' => 'datetime', 'progress_updated_at' => 'datetime', 'processing_started_at' => 'datetime', 'processing_finished_at' => 'datetime', ]; public const STATUS_PENDING = 'pending'; public const STATUS_PARSED = 'parsed'; public const STATUS_REVIEWED = 'reviewed'; public const STATUS_COMPLETED = 'completed'; public const STATUS_PROCESSING = 'processing'; public const STATUS_FAILED = 'failed'; public const STAGE_QUEUED = 'queued'; public const STAGE_SPLITTING = 'splitting'; public const STAGE_AI_PARSING = 'ai_parsing'; public const STAGE_WRITING = 'writing'; public const STAGE_PARSED = 'parsed'; public const STAGE_COMPLETED = 'completed'; public const STAGE_FAILED = 'failed'; public function candidates(): HasMany { return $this->hasMany(PreQuestionCandidate::class, 'import_id'); } public function preQuestions(): HasMany { return $this->hasMany(PreQuestion::class, 'import_id'); } public function getStatusBadgeAttribute(): string { $badges = [ self::STATUS_PENDING => 'gray', self::STATUS_PARSED => 'info', self::STATUS_REVIEWED => 'warning', self::STATUS_COMPLETED => 'success', ]; return $badges[$this->status] ?? 'gray'; } public function getProgressLabelAttribute(): string { $stageLabel = match ($this->progress_stage) { self::STAGE_QUEUED => '已排队', self::STAGE_SPLITTING => '拆题中', self::STAGE_AI_PARSING => 'AI 解析中', self::STAGE_WRITING => '写入候选库', self::STAGE_PARSED => '已解析', self::STAGE_COMPLETED => '已完成', self::STAGE_FAILED => '失败', default => $this->progress_stage ?: '—', }; if (($this->progress_total ?? 0) > 0) { return sprintf( '%s %d/%d', $stageLabel, (int) ($this->progress_current ?? 0), (int) $this->progress_total ); } return $stageLabel; } public function getProgressPercentAttribute(): ?int { $total = (int) ($this->progress_total ?? 0); if ($total <= 0) { return null; } $current = (int) ($this->progress_current ?? 0); return (int) max(0, min(100, round(($current / $total) * 100))); } public function getParsedCountAttribute(): int { if (array_key_exists('parsed_count', $this->attributes)) { return (int) $this->attributes['parsed_count']; } return $this->candidates()->count(); } public function getAcceptedCountAttribute(): int { if (array_key_exists('accepted_count', $this->attributes)) { return (int) $this->attributes['accepted_count']; } return $this->candidates()->where('is_question_candidate', true)->count(); } /** * 获取切分后的候选题目(新的切分格式) */ public function getSplitCandidatesAttribute(): array { if (!$this->parsed_json) { return []; } $data = json_decode($this->parsed_json, true); return $data['candidates'] ?? []; } /** * 获取统计信息 */ public function getSplitStatisticsAttribute(): array { if (!$this->parsed_json) { return []; } $data = json_decode($this->parsed_json, true); return $data['statistics'] ?? []; } /** * 检查是否已完成 */ public function isCompleted(): bool { return $this->status === self::STATUS_COMPLETED; } /** * 检查是否正在处理 */ public function isProcessing(): bool { return $this->status === self::STATUS_PROCESSING; } /** * 检查是否失败 */ public function isFailed(): bool { return $this->status === self::STATUS_FAILED; } /** * 获取状态标签 */ public function getStatusLabelAttribute(): string { return match ($this->status) { self::STATUS_PENDING => '等待处理', self::STATUS_PROCESSING => '处理中', self::STATUS_COMPLETED => '已完成', self::STATUS_FAILED => '处理失败', self::STATUS_PARSED => '已解析', self::STATUS_REVIEWED => '已审核', default => '未知', }; } /** * 获取状态颜色 */ public function getSplitStatusColorAttribute(): string { return match ($this->status) { self::STATUS_PENDING => 'gray', self::STATUS_PROCESSING => 'warning', self::STATUS_COMPLETED => 'success', self::STATUS_FAILED => 'danger', self::STATUS_PARSED => 'info', self::STATUS_REVIEWED => 'primary', default => 'gray', }; } public function parseFilename(): array { if (empty($this->file_name)) { return []; } $base = pathinfo((string) $this->file_name, PATHINFO_FILENAME); $parts = array_map('trim', explode('_', $base)); if (count($parts) < 4) { return []; } $series = $parts[0] ?? null; $grade = isset($parts[1]) && is_numeric($parts[1]) ? (int) $parts[1] : null; $termFlag = isset($parts[2]) && is_numeric($parts[2]) ? (int) $parts[2] : null; $subject = $parts[3] ?? null; $name = trim(implode('_', array_slice($parts, 4))); $term = match ($termFlag) { 1 => '上册', 2 => '下册', 0 => '上下册', default => null, }; return [ 'series' => $series, 'grade' => $grade, 'term' => $term, 'subject' => $subject, 'name' => $name !== '' ? $name : $base, ]; } }