catalogTree = $apiService->getTextbookCatalog((int) $this->record->id, 'tree'); \Log::debug('ViewTextbook: catalog loaded', [ 'record_id' => $this->record->id, 'elapsed_ms' => (int) ((microtime(true) - $catalogStartedAt) * 1000), 'node_count' => count($this->catalogTree), ]); $seriesName = $this->record->series->name ?? null; $linkedStartedAt = microtime(true); $this->linkedPapers = SourcePaper::query() ->when($seriesName, fn ($query) => $query->where('textbook_series', $seriesName)) ->latest('updated_at') ->take(8) ->get() ->map(fn ($paper) => [ 'id' => $paper->id, 'title' => $paper->title ?: $paper->full_title ?: '未命名卷子', 'chapter' => $paper->chapter, 'grade' => $paper->grade, 'term' => $paper->term, 'source_type' => $paper->source_type, 'updated_at' => $paper->updated_at, ]) ->toArray(); \Log::debug('ViewTextbook: linked papers loaded', [ 'record_id' => $this->record->id, 'elapsed_ms' => (int) ((microtime(true) - $linkedStartedAt) * 1000), 'count' => count($this->linkedPapers), ]); // 用 SQL 聚合替代全量 get()->each(),避免教材关联卷子多时页面卡顿。 $coverageStartedAt = microtime(true); $this->catalogCoverage = SourcePaper::query() ->where('textbook_id', $this->record->id) ->whereRaw("JSON_EXTRACT(meta, '$.catalog_node_id') IS NOT NULL") ->selectRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.catalog_node_id')) AS catalog_node_id, COUNT(*) AS aggregate_count") ->groupBy('catalog_node_id') ->pluck('aggregate_count', 'catalog_node_id') ->map(fn ($count) => (int) $count) ->all(); $this->unlinkedPaperCount = SourcePaper::query() ->where('textbook_id', $this->record->id) ->where(function ($query) { $query ->whereNull('meta') ->orWhereRaw("JSON_EXTRACT(meta, '$.catalog_node_id') IS NULL"); }) ->count(); \Log::debug('ViewTextbook: coverage computed', [ 'record_id' => $this->record->id, 'elapsed_ms' => (int) ((microtime(true) - $coverageStartedAt) * 1000), 'coverage_nodes' => count($this->catalogCoverage), 'unlinked_count' => $this->unlinkedPaperCount, ]); \Log::debug('ViewTextbook: mount done', [ 'record_id' => $this->record->id, 'total_elapsed_ms' => (int) ((microtime(true) - $startedAt) * 1000), ]); } }