|
|
@@ -6,8 +6,13 @@ use App\Models\MarkdownImport;
|
|
|
use App\Models\SourcePaper;
|
|
|
use App\Models\Textbook;
|
|
|
use App\Models\TextbookCatalog;
|
|
|
+use App\Services\ImportInferenceService;
|
|
|
+use App\Models\TextbookSeries;
|
|
|
use Filament\Pages\Page;
|
|
|
use Illuminate\Support\Arr;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Filament\Notifications\Notification;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
|
|
|
class MarkdownImportWorkbench extends Page
|
|
|
{
|
|
|
@@ -18,11 +23,18 @@ class MarkdownImportWorkbench extends Page
|
|
|
|
|
|
protected string $view = 'filament.pages.markdown-import-workbench';
|
|
|
|
|
|
+ protected ?ImportInferenceService $inferenceService = null;
|
|
|
+
|
|
|
+ protected function inferenceService(): ImportInferenceService
|
|
|
+ {
|
|
|
+ return $this->inferenceService ??= app(ImportInferenceService::class);
|
|
|
+ }
|
|
|
+
|
|
|
public ?int $importId = null;
|
|
|
public ?int $selectedPaperId = null;
|
|
|
public array $selectedIds = [];
|
|
|
public string $search = '';
|
|
|
- public string $groupBy = 'bundle';
|
|
|
+ public string $groupBy = 'paper';
|
|
|
public bool $dense = false;
|
|
|
public bool $filenameValid = true;
|
|
|
public array $filenameParsed = [];
|
|
|
@@ -38,12 +50,14 @@ class MarkdownImportWorkbench extends Page
|
|
|
'source_year' => null,
|
|
|
'textbook_id' => null,
|
|
|
'textbook_series' => null,
|
|
|
+ 'textbook_series_id' => null,
|
|
|
'source_name' => null,
|
|
|
'source_page' => null,
|
|
|
+ 'subject' => '数学', // 默认学科
|
|
|
'tags' => '',
|
|
|
'bundle_key' => null,
|
|
|
'expected_count' => null,
|
|
|
- 'catalog_node_id' => null,
|
|
|
+ 'catalog_node_ids' => [],
|
|
|
];
|
|
|
|
|
|
public array $batch = [
|
|
|
@@ -60,9 +74,11 @@ class MarkdownImportWorkbench extends Page
|
|
|
'tags' => '',
|
|
|
'bundle_key' => null,
|
|
|
'expected_count' => null,
|
|
|
- 'catalog_node_id' => null,
|
|
|
+ 'catalog_node_ids' => [],
|
|
|
];
|
|
|
|
|
|
+ private array $catalogNodeCache = [];
|
|
|
+
|
|
|
public function mount(): void
|
|
|
{
|
|
|
$this->importId = request()->integer('import_id');
|
|
|
@@ -138,27 +154,170 @@ class MarkdownImportWorkbench extends Page
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- $meta = $paper->meta ?? [];
|
|
|
+ Log::info('MarkdownImportWorkbench select paper', [
|
|
|
+ 'import_id' => $this->importId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ 'paper_title' => $paper->title,
|
|
|
+ 'selected_ids' => $this->selectedIds,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $parsed = $this->parseImportFilename();
|
|
|
$this->selectedPaperId = $paperId;
|
|
|
+ $meta = $paper->meta ?? [];
|
|
|
+
|
|
|
+ // 核心修正:直接基于物理 series_id 初始化,如果没有则尝试从文件名或标题解析
|
|
|
+ $finalSeriesId = $paper->series_id;
|
|
|
+ $finalSeriesName = null;
|
|
|
+
|
|
|
+ if (!$finalSeriesId) {
|
|
|
+ // 兜底1:从文件名解析
|
|
|
+ if (!empty($parsed['series'])) {
|
|
|
+ $formalSeries = $this->inferenceService()->resolveSeries($parsed['series']);
|
|
|
+ $finalSeriesId = $formalSeries?->id;
|
|
|
+ $finalSeriesName = $formalSeries ? $formalSeries->name : $parsed['series'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 兜底2:从标题模糊推断 (应对数据库数据未对齐的情况)
|
|
|
+ if (!$finalSeriesId) {
|
|
|
+ $title = $paper->full_title ?: $paper->title;
|
|
|
+ $formalSeries = $this->inferenceService()->resolveSeries($title);
|
|
|
+ $finalSeriesId = $formalSeries?->id;
|
|
|
+ $finalSeriesName = $formalSeries?->name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($finalSeriesId && !$finalSeriesName) {
|
|
|
+ $finalSeriesName = TextbookSeries::find($finalSeriesId)?->name;
|
|
|
+ }
|
|
|
+
|
|
|
+ $resolvedTextbook = null;
|
|
|
+ if (!$paper->textbook_id && $finalSeriesId) {
|
|
|
+ $resolvedTextbook = $this->inferenceService()->findBestTextbook([
|
|
|
+ 'series_id' => $finalSeriesId,
|
|
|
+ 'grade' => $paper->grade ?: ($parsed['grade'] ?? null),
|
|
|
+ 'term' => $paper->term ?: ($parsed['term'] ?? null),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ $initialCatalogIds = (array) (Arr::get($meta, 'catalog_node_ids') ?: (Arr::get($meta, 'catalog_node_id') ? [Arr::get($meta, 'catalog_node_id')] : []));
|
|
|
+ $filteredCatalogIds = $this->filterCatalogNodeIdsForTextbook($paper->textbook_id ?: ($resolvedTextbook?->id ?? null), $initialCatalogIds);
|
|
|
+ if ($initialCatalogIds !== $filteredCatalogIds && !empty($initialCatalogIds)) {
|
|
|
+ Log::warning('MarkdownImportWorkbench catalog nodes not in textbook', [
|
|
|
+ 'paper_id' => $paper->id,
|
|
|
+ 'paper_title' => $paper->title,
|
|
|
+ 'textbook_id' => $paper->textbook_id,
|
|
|
+ 'original_catalog_node_ids' => $initialCatalogIds,
|
|
|
+ 'filtered_catalog_node_ids' => $filteredCatalogIds,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
$this->form = [
|
|
|
'title' => $paper->title,
|
|
|
'edition' => $paper->edition,
|
|
|
- 'grade' => $paper->grade,
|
|
|
- 'term' => $paper->term,
|
|
|
+ 'grade' => $paper->grade ?: ($parsed['grade'] ?? null),
|
|
|
+ 'term' => $paper->term ?: ($parsed['term'] ?? null),
|
|
|
'chapter' => $paper->chapter,
|
|
|
'source_type' => $paper->source_type,
|
|
|
'source_year' => $paper->source_year,
|
|
|
- 'textbook_id' => $paper->textbook_id,
|
|
|
- 'textbook_series' => $paper->textbook_series,
|
|
|
- 'source_name' => Arr::get($meta, 'source_name'),
|
|
|
+ 'textbook_id' => $paper->textbook_id ?: ($resolvedTextbook?->id ?? null),
|
|
|
+ 'textbook_series' => $finalSeriesName,
|
|
|
+ 'textbook_series_id' => $finalSeriesId,
|
|
|
+ 'source_name' => Arr::get($meta, 'source_name') ?: ($parsed['name'] ?? null),
|
|
|
'source_page' => Arr::get($meta, 'source_page'),
|
|
|
'tags' => implode(',', Arr::get($meta, 'tags', [])),
|
|
|
'bundle_key' => Arr::get($meta, 'bundle_key'),
|
|
|
'expected_count' => Arr::get($meta, 'expected_count'),
|
|
|
- 'catalog_node_id' => Arr::get($meta, 'catalog_node_id'),
|
|
|
+ 'catalog_node_ids' => $filteredCatalogIds,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ public function updatedSelectedIds(): void
|
|
|
+ {
|
|
|
+ if (empty($this->selectedIds) || $this->selectedPaperId === null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($this->isBatchEmpty()) {
|
|
|
+ $this->seedBatchFromCurrent();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public function updated($name, $value): void
|
|
|
+ {
|
|
|
+ if (!$this->selectedPaperId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 教材属性联动:当系列、年级、学期改变时,自动匹配最合适的教材
|
|
|
+ */
|
|
|
+ public function updatedFormGrade(): void { $this->reInferTextbook(); }
|
|
|
+ public function updatedFormTerm(): void { $this->reInferTextbook(); }
|
|
|
+ public function updatedFormTextbookSeriesId(): void
|
|
|
+ {
|
|
|
+ // 核心联动:系列 ID 变动,清空教材并重推
|
|
|
+ $this->form['textbook_id'] = null;
|
|
|
+ $this->form['catalog_node_ids'] = [];
|
|
|
+
|
|
|
+ // 同时同步一下显示名称 (虽然逻辑以 ID 为准,但保留名称用于前端显示或保存)
|
|
|
+ $series = TextbookSeries::find($this->form['textbook_series_id']);
|
|
|
+ $this->form['textbook_series'] = $series?->name;
|
|
|
+
|
|
|
+ $this->reInferTextbook();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function reInferTextbook(): void
|
|
|
+ {
|
|
|
+ if (!empty($this->form['textbook_id'])) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $best = $this->inferenceService()->findBestTextbook($this->form);
|
|
|
+ if ($best) {
|
|
|
+ // 无论 ID 是否改变,都强制执行一次属性校准,确保“一环扣一环”
|
|
|
+ $this->syncTextbookAttributes($best);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function syncTextbookAttributes(Textbook $textbook): void
|
|
|
+ {
|
|
|
+ $this->form['textbook_id'] = $textbook->id;
|
|
|
+ $this->form['grade'] = (string)$textbook->grade;
|
|
|
+ $this->form['term'] = $this->semesterToTerm($textbook->semester);
|
|
|
+ $this->form['textbook_series_id'] = $textbook->series_id;
|
|
|
+
|
|
|
+ $series = $textbook->getRelation('series') ?: $textbook->series()->first();
|
|
|
+ $seriesName = $textbook->track ?: ($series?->name ?? null);
|
|
|
+ if ($seriesName) {
|
|
|
+ $this->form['textbook_series'] = $seriesName;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->savePaper();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function updatedFormTextbookId($value): void
|
|
|
+ {
|
|
|
+ if (!$this->selectedPaperId || !$value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 权威源:从数据库获取该教材的所有官方属性
|
|
|
+ $textbook = Textbook::query()->with('series')->find($value);
|
|
|
+ if ($textbook) {
|
|
|
+ $this->syncTextbookAttributes($textbook);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function semesterToTerm(?int $semester): ?string
|
|
|
+ {
|
|
|
+ return match ($semester) {
|
|
|
+ 1 => '上册',
|
|
|
+ 2 => '下册',
|
|
|
+ default => null,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
public function seedBatchFromCurrent(): void
|
|
|
{
|
|
|
$this->batch = [
|
|
|
@@ -169,86 +328,115 @@ class MarkdownImportWorkbench extends Page
|
|
|
'source_type' => $this->form['source_type'] ?? null,
|
|
|
'source_year' => $this->form['source_year'] ?? null,
|
|
|
'textbook_id' => $this->form['textbook_id'] ?? null,
|
|
|
+ 'textbook_series_id' => $this->form['textbook_series_id'] ?? null,
|
|
|
'textbook_series' => $this->form['textbook_series'] ?? null,
|
|
|
'source_name' => $this->form['source_name'] ?? null,
|
|
|
'source_page' => $this->form['source_page'] ?? null,
|
|
|
'tags' => $this->form['tags'] ?? '',
|
|
|
'bundle_key' => $this->form['bundle_key'] ?? null,
|
|
|
'expected_count' => $this->form['expected_count'] ?? null,
|
|
|
- 'catalog_node_id' => $this->form['catalog_node_id'] ?? null,
|
|
|
+ 'catalog_node_ids' => $this->form['catalog_node_ids'] ?? [],
|
|
|
];
|
|
|
}
|
|
|
|
|
|
- public function savePaper(): void
|
|
|
+ public function savePaper(bool $silent = false): void
|
|
|
{
|
|
|
$paper = $this->selectedPaper();
|
|
|
if (!$paper) {
|
|
|
+ Log::warning('MarkdownImportWorkbench save failed: no paper selected', [
|
|
|
+ 'import_id' => $this->importId,
|
|
|
+ 'selected_paper_id' => $this->selectedPaperId,
|
|
|
+ ]);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ $selectedIds = array_values(array_filter(array_unique(array_map('intval', $this->selectedIds))));
|
|
|
+
|
|
|
$meta = $paper->meta ?? [];
|
|
|
$meta['source_name'] = $this->form['source_name'] ?? null;
|
|
|
$meta['source_page'] = $this->form['source_page'] ?? null;
|
|
|
$meta['tags'] = $this->explodeTags($this->form['tags'] ?? '');
|
|
|
$meta['bundle_key'] = $this->form['bundle_key'] ?? null;
|
|
|
$meta['expected_count'] = $this->form['expected_count'] ?? null;
|
|
|
- $meta['catalog_node_id'] = $this->form['catalog_node_id'] ?? null;
|
|
|
+
|
|
|
+ // 关键修正:确保目录 ID 是干净的整型数组,解决保存失效
|
|
|
+ $catalogNodeIds = $this->normalizeCatalogNodeIds($this->form['catalog_node_ids'] ?? []);
|
|
|
+ $catalogNodeIds = $this->filterCatalogNodeIdsForTextbook($updates['textbook_id'] ?? null, $catalogNodeIds);
|
|
|
+ $meta['catalog_node_ids'] = $catalogNodeIds;
|
|
|
+
|
|
|
+ // 同时保留旧字段供向后兼容
|
|
|
+ $meta['catalog_node_id'] = !empty($meta['catalog_node_ids']) ? $meta['catalog_node_ids'][0] : null;
|
|
|
+
|
|
|
+ // 核心同步:保存时根据 series_id 确保名称同步
|
|
|
+ if (!empty($this->form['textbook_series_id'])) {
|
|
|
+ $series = TextbookSeries::find($this->form['textbook_series_id']);
|
|
|
+ if ($series) {
|
|
|
+ $this->form['textbook_series'] = $series->name;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- $paper->update([
|
|
|
- 'title' => $this->form['title'] ?? null,
|
|
|
- 'edition' => $this->form['edition'] ?? null,
|
|
|
- 'grade' => $this->form['grade'] ?? null,
|
|
|
- 'term' => $this->form['term'] ?? null,
|
|
|
- 'chapter' => $this->form['chapter'] ?? null,
|
|
|
- 'source_type' => $this->form['source_type'] ?? null,
|
|
|
- 'source_year' => $this->form['source_year'] ?? null,
|
|
|
- 'textbook_id' => $this->form['textbook_id'] ?? null,
|
|
|
- 'textbook_series' => $this->form['textbook_series'] ?? null,
|
|
|
- 'meta' => $meta,
|
|
|
+ $fields = [
|
|
|
+ 'title', 'edition', 'grade', 'term', 'chapter', 'source_type',
|
|
|
+ 'source_year', 'textbook_id'
|
|
|
+ ];
|
|
|
+
|
|
|
+ $updates = [];
|
|
|
+ foreach ($fields as $field) {
|
|
|
+ $value = $this->form[$field] ?? null;
|
|
|
+ $updates[$field] = ($value === '' ? null : $value);
|
|
|
+ }
|
|
|
+ $updates['series_id'] = $this->form['textbook_series_id'] ?? null;
|
|
|
+ $updates['meta'] = $meta;
|
|
|
+
|
|
|
+ Log::info('MarkdownImportWorkbench saving paper', [
|
|
|
+ 'import_id' => $this->importId,
|
|
|
+ 'paper_id' => $paper->id,
|
|
|
+ 'paper_title' => $paper->title,
|
|
|
+ 'catalog_node_ids' => $catalogNodeIds,
|
|
|
+ 'textbook_id' => $updates['textbook_id'] ?? null,
|
|
|
+ 'series_id' => $updates['series_id'] ?? null,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $paper->update($updates);
|
|
|
+
|
|
|
+ Log::info('MarkdownImportWorkbench saved paper', [
|
|
|
+ 'import_id' => $this->importId,
|
|
|
+ 'paper_id' => $paper->id,
|
|
|
+ 'meta_catalog_node_ids' => $meta['catalog_node_ids'] ?? [],
|
|
|
]);
|
|
|
+
|
|
|
+ if (count($selectedIds) > 1) {
|
|
|
+ $otherIds = array_values(array_diff($selectedIds, [$paper->id]));
|
|
|
+ if (!empty($otherIds)) {
|
|
|
+ $this->seedBatchFromCurrent();
|
|
|
+ $this->applyBatchToIds($otherIds);
|
|
|
+
|
|
|
+ Log::info('MarkdownImportWorkbench batch saved papers', [
|
|
|
+ 'import_id' => $this->importId,
|
|
|
+ 'paper_ids' => $otherIds,
|
|
|
+ 'catalog_node_ids' => $this->normalizeCatalogNodeIds($this->batch['catalog_node_ids'] ?? []),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->selectedIds = [];
|
|
|
+
|
|
|
+ if (!$silent) {
|
|
|
+ Notification::make()
|
|
|
+ ->title('保存成功')
|
|
|
+ ->success()
|
|
|
+ ->send();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+
|
|
|
public function applyBatch(): void
|
|
|
{
|
|
|
if (empty($this->selectedIds)) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- $updates = array_filter([
|
|
|
- 'edition' => $this->batch['edition'] ?? null,
|
|
|
- 'grade' => $this->batch['grade'] ?? null,
|
|
|
- 'term' => $this->batch['term'] ?? null,
|
|
|
- 'chapter' => $this->batch['chapter'] ?? null,
|
|
|
- 'source_type' => $this->batch['source_type'] ?? null,
|
|
|
- 'source_year' => $this->batch['source_year'] ?? null,
|
|
|
- 'textbook_id' => $this->batch['textbook_id'] ?? null,
|
|
|
- 'textbook_series' => $this->batch['textbook_series'] ?? null,
|
|
|
- ], fn ($value) => $value !== null && $value !== '');
|
|
|
-
|
|
|
- foreach (SourcePaper::query()->whereIn('id', $this->selectedIds)->get() as $paper) {
|
|
|
- $meta = $paper->meta ?? [];
|
|
|
-
|
|
|
- if (!empty($this->batch['source_name'])) {
|
|
|
- $meta['source_name'] = $this->batch['source_name'];
|
|
|
- }
|
|
|
- if (!empty($this->batch['source_page'])) {
|
|
|
- $meta['source_page'] = $this->batch['source_page'];
|
|
|
- }
|
|
|
- if (!empty($this->batch['tags'])) {
|
|
|
- $meta['tags'] = $this->explodeTags($this->batch['tags']);
|
|
|
- }
|
|
|
- if (!empty($this->batch['bundle_key'])) {
|
|
|
- $meta['bundle_key'] = $this->batch['bundle_key'];
|
|
|
- }
|
|
|
- if (!empty($this->batch['expected_count'])) {
|
|
|
- $meta['expected_count'] = $this->batch['expected_count'];
|
|
|
- }
|
|
|
- if (!empty($this->batch['catalog_node_id'])) {
|
|
|
- $meta['catalog_node_id'] = $this->batch['catalog_node_id'];
|
|
|
- }
|
|
|
-
|
|
|
- $paper->update(array_merge($updates, ['meta' => $meta]));
|
|
|
- }
|
|
|
+ $this->applyBatchToIds($this->selectedIds);
|
|
|
}
|
|
|
|
|
|
public function autoInfer(): void
|
|
|
@@ -260,7 +448,17 @@ class MarkdownImportWorkbench extends Page
|
|
|
|
|
|
$parsed = $this->parseImportFilename();
|
|
|
if (!empty($parsed)) {
|
|
|
- $this->form['textbook_series'] = $this->form['textbook_series'] ?: $parsed['series'];
|
|
|
+ // 关键:推断出的系列必须经过正式化 ID 锁定,否则无法触发联动
|
|
|
+ if (empty($this->form['textbook_series_id']) && !empty($parsed['series'])) {
|
|
|
+ $formal = $this->inferenceService()->resolveSeries($parsed['series']);
|
|
|
+ if ($formal) {
|
|
|
+ $this->form['textbook_series_id'] = $formal->id;
|
|
|
+ $this->form['textbook_series'] = $formal->name;
|
|
|
+ } else {
|
|
|
+ $this->form['textbook_series'] = $parsed['series'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
$this->form['grade'] = $this->form['grade'] ?: $parsed['grade'];
|
|
|
$this->form['term'] = $this->form['term'] ?: $parsed['term'];
|
|
|
$this->form['source_name'] = $this->form['source_name'] ?: $parsed['name'];
|
|
|
@@ -270,9 +468,21 @@ class MarkdownImportWorkbench extends Page
|
|
|
$raw = (string) ($paper->raw_markdown ?? '');
|
|
|
$context = $title . ' ' . $raw;
|
|
|
|
|
|
- $this->form['term'] = $this->inferTerm($context) ?? $this->form['term'];
|
|
|
- $this->form['grade'] = $this->inferGrade($context) ?? $this->form['grade'];
|
|
|
- $this->form['chapter'] = $this->inferChapter($context) ?? $this->form['chapter'];
|
|
|
+ $this->form['term'] = $this->inferenceService()->inferTerm($context) ?? $this->form['term'];
|
|
|
+ $this->form['grade'] = $this->inferenceService()->inferGrade($context) ?? $this->form['grade'];
|
|
|
+ $this->form['chapter'] = $this->inferenceService()->inferChapter($context) ?? $this->form['chapter'];
|
|
|
+ $this->form['source_type'] = $this->inferenceService()->inferSourceType($context) ?? $this->form['source_type'];
|
|
|
+
|
|
|
+ if (empty($this->form['catalog_node_ids'])) {
|
|
|
+ $matchedCatalog = $this->inferenceService()->matchCatalogNodeId($context, $this->form['textbook_id']);
|
|
|
+ if ($matchedCatalog) {
|
|
|
+ $this->form['catalog_node_ids'] = [$matchedCatalog];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行推断后,立即触发教材重新关联和保存,确保“一环扣一环”
|
|
|
+ $this->reInferTextbook();
|
|
|
+ $this->savePaper();
|
|
|
}
|
|
|
|
|
|
public function autoBundleKey(): void
|
|
|
@@ -308,9 +518,9 @@ class MarkdownImportWorkbench extends Page
|
|
|
foreach (SourcePaper::query()->whereIn('id', $this->selectedIds)->get() as $paper) {
|
|
|
$context = (string) ($paper->title ?? $paper->full_title ?? '') . ' ' . (string) ($paper->raw_markdown ?? '');
|
|
|
$updates = array_filter([
|
|
|
- 'term' => $this->inferTerm($context),
|
|
|
- 'grade' => $this->inferGrade($context),
|
|
|
- 'chapter' => $this->inferChapter($context),
|
|
|
+ 'term' => $this->inferenceService()->inferTerm($context),
|
|
|
+ 'grade' => $this->inferenceService()->inferGrade($context),
|
|
|
+ 'chapter' => $this->inferenceService()->inferChapter($context),
|
|
|
], fn ($value) => $value !== null && $value !== '');
|
|
|
|
|
|
if (!empty($parsed)) {
|
|
|
@@ -329,6 +539,28 @@ class MarkdownImportWorkbench extends Page
|
|
|
if (!empty($parsed['name']) && empty($meta['source_name'])) {
|
|
|
$meta['source_name'] = $parsed['name'];
|
|
|
}
|
|
|
+ if (empty($meta['catalog_node_ids'])) {
|
|
|
+ $candidateTextbookId = $updates['textbook_id'] ?? $paper->textbook_id ?? null;
|
|
|
+
|
|
|
+ // 如果没有教材 ID,实时推断一个
|
|
|
+ if (!$candidateTextbookId) {
|
|
|
+ $best = $this->inferenceService()->findBestTextbook([
|
|
|
+ 'series_id' => $updates['textbook_series_id'] ?? null,
|
|
|
+ 'grade' => $updates['grade'] ?? $paper->grade,
|
|
|
+ 'term' => $updates['term'] ?? $paper->term,
|
|
|
+ ]);
|
|
|
+ if ($best) {
|
|
|
+ $candidateTextbookId = $best->id;
|
|
|
+ $updates['textbook_id'] = $best->id;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $matchedCatalog = $this->inferenceService()->matchCatalogNodeId($context, $candidateTextbookId);
|
|
|
+ if ($matchedCatalog) {
|
|
|
+ $meta['catalog_node_ids'] = [$matchedCatalog];
|
|
|
+ $meta['catalog_node_id'] = $matchedCatalog;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
if (!empty($updates)) {
|
|
|
$updates['meta'] = $meta;
|
|
|
@@ -338,6 +570,51 @@ class MarkdownImportWorkbench extends Page
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ public function mergeSelectedPapers(): void
|
|
|
+ {
|
|
|
+ if (count($this->selectedIds) < 2) {
|
|
|
+ $this->dispatch('notify', ['type' => 'warning', 'message' => '请至少选择两套卷子进行合并']);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $papers = SourcePaper::query()
|
|
|
+ ->whereIn('id', $this->selectedIds)
|
|
|
+ ->orderBy('order')
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ $target = $papers->shift(); // 第一套作为目标
|
|
|
+
|
|
|
+ DB::transaction(function () use ($target, $papers) {
|
|
|
+ foreach ($papers as $source) {
|
|
|
+ // 1. 移动所有候选题目
|
|
|
+ DB::table('pre_question_candidates')
|
|
|
+ ->where('source_paper_id', $source->id)
|
|
|
+ ->update(['source_paper_id' => $target->id]);
|
|
|
+
|
|
|
+ // 2. 移动所有 PaperPart (如果需要)
|
|
|
+ DB::table('paper_parts')
|
|
|
+ ->where('source_paper_id', $source->id)
|
|
|
+ ->update(['source_paper_id' => $target->id]);
|
|
|
+
|
|
|
+ // 3. 追加 Markdown 内容 (可选,但有助于保持记录完整)
|
|
|
+ $target->raw_markdown .= "\n\n" . $source->raw_markdown;
|
|
|
+
|
|
|
+ // 4. 删除原卷子
|
|
|
+ $source->delete();
|
|
|
+ }
|
|
|
+
|
|
|
+ $target->save();
|
|
|
+ });
|
|
|
+
|
|
|
+ $this->selectedIds = [];
|
|
|
+ $this->selectPaper($target->id);
|
|
|
+
|
|
|
+ Notification::make()
|
|
|
+ ->title('卷子合并成功')
|
|
|
+ ->success()
|
|
|
+ ->send();
|
|
|
+ }
|
|
|
|
|
|
public function selectAllVisible(): void
|
|
|
{
|
|
|
@@ -378,8 +655,15 @@ class MarkdownImportWorkbench extends Page
|
|
|
|
|
|
public function textbookOptions(): array
|
|
|
{
|
|
|
- return Textbook::query()
|
|
|
- ->orderBy('id')
|
|
|
+ $query = Textbook::query();
|
|
|
+
|
|
|
+ // 核心联动:基于 series_id 进行物理过滤
|
|
|
+ $seriesId = $this->form['textbook_series_id'] ?? null;
|
|
|
+ if ($seriesId) {
|
|
|
+ $query->where('series_id', $seriesId);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $query->orderBy('id')
|
|
|
->get(['id', 'official_title'])
|
|
|
->mapWithKeys(function ($textbook) {
|
|
|
$title = $textbook->official_title ?: '未命名教材';
|
|
|
@@ -388,13 +672,22 @@ class MarkdownImportWorkbench extends Page
|
|
|
->toArray();
|
|
|
}
|
|
|
|
|
|
- public function catalogOptions(): array
|
|
|
+ public function seriesOptions(): array
|
|
|
{
|
|
|
- if (empty($this->form['textbook_id']) && empty($this->batch['textbook_id'])) {
|
|
|
+ return \App\Models\TextbookSeries::query()
|
|
|
+ ->orderBy('sort_order')
|
|
|
+ ->pluck('name', 'id')
|
|
|
+ ->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function catalogOptions(?int $textbookId = null): array
|
|
|
+ {
|
|
|
+ $textbookId ??= $this->form['textbook_id'] ?? null;
|
|
|
+
|
|
|
+ if (empty($textbookId)) {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
- $textbookId = $this->form['textbook_id'] ?: $this->batch['textbook_id'];
|
|
|
$nodes = TextbookCatalog::query()
|
|
|
->where('textbook_id', $textbookId)
|
|
|
->orderBy('sort_order')
|
|
|
@@ -432,9 +725,11 @@ class MarkdownImportWorkbench extends Page
|
|
|
->where('textbook_id', $textbookId)
|
|
|
->get(['meta'])
|
|
|
->each(function ($paper) use (&$coverage) {
|
|
|
- $catalogId = $paper->meta['catalog_node_id'] ?? null;
|
|
|
- if ($catalogId) {
|
|
|
- $coverage[$catalogId] = ($coverage[$catalogId] ?? 0) + 1;
|
|
|
+ $ids = $paper->meta['catalog_node_ids'] ?? (isset($paper->meta['catalog_node_id']) ? [$paper->meta['catalog_node_id']] : []);
|
|
|
+ foreach ((array) $ids as $id) {
|
|
|
+ if ($id) {
|
|
|
+ $coverage[$id] = ($coverage[$id] ?? 0) + 1;
|
|
|
+ }
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -466,9 +761,11 @@ class MarkdownImportWorkbench extends Page
|
|
|
->where('textbook_id', $textbookId)
|
|
|
->get(['meta'])
|
|
|
->each(function ($paper) use (&$coverage) {
|
|
|
- $catalogId = $paper->meta['catalog_node_id'] ?? null;
|
|
|
- if ($catalogId) {
|
|
|
- $coverage[$catalogId] = ($coverage[$catalogId] ?? 0) + 1;
|
|
|
+ $ids = $paper->meta['catalog_node_ids'] ?? (isset($paper->meta['catalog_node_id']) ? [$paper->meta['catalog_node_id']] : []);
|
|
|
+ foreach ((array) $ids as $id) {
|
|
|
+ if ($id) {
|
|
|
+ $coverage[$id] = ($coverage[$id] ?? 0) + 1;
|
|
|
+ }
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -485,6 +782,32 @@ class MarkdownImportWorkbench extends Page
|
|
|
return array_slice($missing, 0, 8);
|
|
|
}
|
|
|
|
|
|
+ public function catalogTitlesForPaper(SourcePaper|array $paper): array
|
|
|
+ {
|
|
|
+ $meta = $paper instanceof SourcePaper ? ($paper->meta ?? []) : ($paper['meta'] ?? []);
|
|
|
+ $textbookId = $paper instanceof SourcePaper ? $paper->textbook_id : ($paper['textbook_id'] ?? null);
|
|
|
+ if (empty($textbookId)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $ids = $this->normalizeCatalogNodeIds($meta['catalog_node_ids'] ?? ($meta['catalog_node_id'] ?? []));
|
|
|
+ if (empty($ids)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $missing = array_values(array_diff($ids, array_keys($this->catalogNodeCache)));
|
|
|
+ if (!empty($missing)) {
|
|
|
+ TextbookCatalog::query()
|
|
|
+ ->whereIn('id', $missing)
|
|
|
+ ->get(['id', 'title'])
|
|
|
+ ->each(function ($node) {
|
|
|
+ $this->catalogNodeCache[(int) $node->id] = $node->title ?: ('目录 #' . $node->id);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return array_values(array_filter(array_map(fn ($id) => $this->catalogNodeCache[$id] ?? null, $ids)));
|
|
|
+ }
|
|
|
+
|
|
|
public function textbookSuggestions(): array
|
|
|
{
|
|
|
$paper = $this->selectedPaper();
|
|
|
@@ -500,64 +823,13 @@ class MarkdownImportWorkbench extends Page
|
|
|
$seriesHint = $paper->textbook_series ?: ($parsed['series'] ?? null);
|
|
|
$subjectHint = $parsed['subject'] ?? null;
|
|
|
|
|
|
- $suggestions = [];
|
|
|
- $textbooks = Textbook::query()->with('series')->get();
|
|
|
- foreach ($textbooks as $textbook) {
|
|
|
- $score = 0;
|
|
|
-
|
|
|
- if ($grade && (int) $textbook->grade === $grade) {
|
|
|
- $score += 3;
|
|
|
- }
|
|
|
- if ($semester && (int) $textbook->semester === $semester) {
|
|
|
- $score += 3;
|
|
|
- }
|
|
|
-
|
|
|
- $official = mb_strtolower((string) $textbook->official_title);
|
|
|
- if ($official !== '' && str_contains($context, $official)) {
|
|
|
- $score += 4;
|
|
|
- }
|
|
|
-
|
|
|
- $aliases = $textbook->aliases ?? [];
|
|
|
- foreach ($aliases as $alias) {
|
|
|
- $alias = mb_strtolower((string) $alias);
|
|
|
- if ($alias !== '' && str_contains($context, $alias)) {
|
|
|
- $score += 2;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- $seriesName = $textbook->series?->name ?? null;
|
|
|
- if ($seriesHint && $seriesName && str_contains((string) $seriesHint, (string) $seriesName)) {
|
|
|
- $score += 5;
|
|
|
- }
|
|
|
-
|
|
|
- if ($subjectHint) {
|
|
|
- $subjectHint = mb_strtolower((string) $subjectHint);
|
|
|
- $official = mb_strtolower((string) $textbook->official_title);
|
|
|
- if ($official !== '' && str_contains($official, $subjectHint)) {
|
|
|
- $score += 1;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if ($score > 0) {
|
|
|
- $suggestions[] = [
|
|
|
- 'id' => $textbook->id,
|
|
|
- 'title' => $textbook->official_title,
|
|
|
- 'series' => $textbook->series?->name ?? '未归类系列',
|
|
|
- 'grade' => $textbook->grade,
|
|
|
- 'semester' => $textbook->semester,
|
|
|
- 'score' => $score,
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- usort($suggestions, fn ($a, $b) => $b['score'] <=> $a['score']);
|
|
|
- return array_slice($suggestions, 0, 5);
|
|
|
+ return $this->inferenceService()->getTextbookSuggestions($paper, $parsed);
|
|
|
}
|
|
|
|
|
|
public function catalogSuggestions(): array
|
|
|
{
|
|
|
$paper = $this->selectedPaper();
|
|
|
- $textbookId = $paper?->textbook_id ?? $this->form['textbook_id'];
|
|
|
+ $textbookId = $paper?->textbook_id ?? ($this->form['textbook_id'] ?? null);
|
|
|
if (!$paper || !$textbookId) {
|
|
|
return [];
|
|
|
}
|
|
|
@@ -596,7 +868,12 @@ class MarkdownImportWorkbench extends Page
|
|
|
|
|
|
public function applyCatalogSuggestion(int $catalogId): void
|
|
|
{
|
|
|
- $this->form['catalog_node_id'] = $catalogId;
|
|
|
+ $ids = $this->form['catalog_node_ids'] ?? [];
|
|
|
+ if (!in_array($catalogId, $ids)) {
|
|
|
+ $ids[] = $catalogId;
|
|
|
+ $this->form['catalog_node_ids'] = $ids;
|
|
|
+ $this->savePaper();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
public function candidateCountFor(SourcePaper $paper): int
|
|
|
@@ -604,33 +881,39 @@ class MarkdownImportWorkbench extends Page
|
|
|
return (int) ($paper->candidates_count ?? 0);
|
|
|
}
|
|
|
|
|
|
- private function inferTerm(string $context): ?string
|
|
|
+ public function checkCompleteness(): array
|
|
|
{
|
|
|
- if (str_contains($context, '上册') || str_contains($context, '上学期')) {
|
|
|
- return '上册';
|
|
|
- }
|
|
|
- if (str_contains($context, '下册') || str_contains($context, '下学期')) {
|
|
|
- return '下册';
|
|
|
+ $paper = $this->selectedPaper();
|
|
|
+ if (!$paper) {
|
|
|
+ return [];
|
|
|
}
|
|
|
- return null;
|
|
|
- }
|
|
|
|
|
|
- private function inferGrade(string $context): ?string
|
|
|
- {
|
|
|
- foreach (['七年级' => '7', '八年级' => '8', '九年级' => '9', '高一' => '10', '高二' => '11', '高三' => '12'] as $label => $value) {
|
|
|
- if (str_contains($context, $label)) {
|
|
|
- return $value;
|
|
|
+ $candidates = $paper->candidates()
|
|
|
+ ->where('is_question_candidate', true)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ $service = app(QuestionCandidateToQuestionService::class);
|
|
|
+ $total = $candidates->count();
|
|
|
+ $invalid = 0;
|
|
|
+ $issueStats = [];
|
|
|
+
|
|
|
+ foreach ($candidates as $candidate) {
|
|
|
+ $errors = $service->validateCandidate($candidate);
|
|
|
+ if (!empty($errors)) {
|
|
|
+ $invalid++;
|
|
|
+ foreach ($errors as $error) {
|
|
|
+ $issueStats[$error] = ($issueStats[$error] ?? 0) + 1;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- return null;
|
|
|
- }
|
|
|
|
|
|
- private function inferChapter(string $context): ?string
|
|
|
- {
|
|
|
- if (preg_match('/第[一二三四五六七八九十]+章[^\\n]*/u', $context, $match)) {
|
|
|
- return $match[0];
|
|
|
- }
|
|
|
- return null;
|
|
|
+ return [
|
|
|
+ 'total' => $total,
|
|
|
+ 'valid' => $total - $invalid,
|
|
|
+ 'invalid' => $invalid,
|
|
|
+ 'issues' => $issueStats,
|
|
|
+ 'is_ready' => $total > 0 && $invalid === 0,
|
|
|
+ ];
|
|
|
}
|
|
|
|
|
|
private function explodeTags(string $tags): array
|
|
|
@@ -640,16 +923,7 @@ class MarkdownImportWorkbench extends Page
|
|
|
|
|
|
private function termToSemester(?string $term): ?int
|
|
|
{
|
|
|
- if (!$term) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- if (str_contains($term, '上')) {
|
|
|
- return 1;
|
|
|
- }
|
|
|
- if (str_contains($term, '下')) {
|
|
|
- return 2;
|
|
|
- }
|
|
|
- return null;
|
|
|
+ return $this->inferenceService()->termToSemester($term);
|
|
|
}
|
|
|
|
|
|
private function buildBundleKey(SourcePaper $paper): string
|
|
|
@@ -693,28 +967,51 @@ class MarkdownImportWorkbench extends Page
|
|
|
}
|
|
|
|
|
|
$parsed = $this->filenameParsed;
|
|
|
+ $resolvedTextbook = $this->inferenceService()->resolveTextbookFromFilename($parsed);
|
|
|
$papers = SourcePaper::query()
|
|
|
->whereHas('candidates', fn ($q) => $q->where('import_id', $this->importId))
|
|
|
->get();
|
|
|
|
|
|
foreach ($papers as $paper) {
|
|
|
$meta = $paper->meta ?? [];
|
|
|
- if (!empty($meta['filename_defaults_applied'])) {
|
|
|
+ $hasNumericGrade = is_numeric((string) $paper->grade);
|
|
|
+ $hasAllDefaults = !empty($paper->textbook_id)
|
|
|
+ && !empty($paper->textbook_series)
|
|
|
+ && $hasNumericGrade
|
|
|
+ && !empty($paper->term);
|
|
|
+ if (!empty($meta['filename_defaults_applied']) && $hasAllDefaults) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
$updates = [];
|
|
|
|
|
|
if (empty($paper->textbook_series) && !empty($parsed['series'])) {
|
|
|
- $updates['textbook_series'] = $parsed['series'];
|
|
|
+ $formal = $this->inferenceService()->resolveSeries($parsed['series']);
|
|
|
+ $updates['textbook_series_id'] = $formal ? $formal->id : null;
|
|
|
+ $updates['textbook_series'] = $formal ? $formal->name : $parsed['series'];
|
|
|
}
|
|
|
- if (empty($paper->grade) && !empty($parsed['grade'])) {
|
|
|
+
|
|
|
+ if (!empty($parsed['grade']) && empty($paper->grade)) {
|
|
|
$updates['grade'] = $parsed['grade'];
|
|
|
}
|
|
|
- if (empty($paper->term) && !empty($parsed['term'])) {
|
|
|
+
|
|
|
+ if (!empty($parsed['term']) && empty($paper->term)) {
|
|
|
$updates['term'] = $parsed['term'];
|
|
|
}
|
|
|
|
|
|
+ // 在应用文件名默认值时,也触发一次教材重定向
|
|
|
+ if (empty($paper->textbook_id)) {
|
|
|
+ $best = $this->inferenceService()->findBestTextbook([
|
|
|
+ 'series_id' => $updates['textbook_series_id'] ?? null,
|
|
|
+ 'textbook_series' => $updates['textbook_series'] ?? $paper->textbook_series,
|
|
|
+ 'grade' => $updates['grade'] ?? $paper->grade,
|
|
|
+ 'term' => $updates['term'] ?? $paper->term,
|
|
|
+ ]);
|
|
|
+ if ($best) {
|
|
|
+ $updates['textbook_id'] = $best->id;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (empty($meta['source_name']) && !empty($parsed['name'])) {
|
|
|
$meta['source_name'] = $parsed['name'];
|
|
|
}
|
|
|
@@ -723,9 +1020,6 @@ class MarkdownImportWorkbench extends Page
|
|
|
$meta['filename_defaults_applied'] = true;
|
|
|
$updates['meta'] = $meta;
|
|
|
$paper->update($updates);
|
|
|
- } elseif (!empty($meta)) {
|
|
|
- $meta['filename_defaults_applied'] = true;
|
|
|
- $paper->update(['meta' => $meta]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -735,4 +1029,117 @@ class MarkdownImportWorkbench extends Page
|
|
|
$import = $this->importRecord();
|
|
|
return $import?->parseFilename() ?? [];
|
|
|
}
|
|
|
+
|
|
|
+ private function normalizeGradeForForm($grade, array $parsed): ?int
|
|
|
+ {
|
|
|
+ if (is_numeric($grade)) {
|
|
|
+ return (int) $grade;
|
|
|
+ }
|
|
|
+
|
|
|
+ $map = [
|
|
|
+ '七年级' => 7,
|
|
|
+ '八年级' => 8,
|
|
|
+ '九年级' => 9,
|
|
|
+ '高一' => 10,
|
|
|
+ '高二' => 11,
|
|
|
+ '高三' => 12,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $grade = is_string($grade) ? trim($grade) : '';
|
|
|
+ if ($grade !== '' && isset($map[$grade])) {
|
|
|
+ return $map[$grade];
|
|
|
+ }
|
|
|
+
|
|
|
+ $parsedGrade = $parsed['grade'] ?? null;
|
|
|
+ return is_numeric($parsedGrade) ? (int) $parsedGrade : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function normalizeCatalogNodeIds($value): array
|
|
|
+ {
|
|
|
+ $ids = is_array($value) ? $value : [$value];
|
|
|
+ $ids = array_values(array_unique(array_map('intval', array_filter($ids))));
|
|
|
+ return $ids;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function filterCatalogNodeIdsForTextbook(?int $textbookId, array $ids): array
|
|
|
+ {
|
|
|
+ $ids = $this->normalizeCatalogNodeIds($ids);
|
|
|
+ if (empty($textbookId) || empty($ids)) {
|
|
|
+ return $ids;
|
|
|
+ }
|
|
|
+
|
|
|
+ $validIds = TextbookCatalog::query()
|
|
|
+ ->where('textbook_id', $textbookId)
|
|
|
+ ->whereIn('id', $ids)
|
|
|
+ ->pluck('id')
|
|
|
+ ->map(fn ($id) => (int) $id)
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ return $validIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function applyBatchToIds(array $ids): void
|
|
|
+ {
|
|
|
+ $targetIds = array_values(array_filter(array_unique(array_map('intval', $ids))));
|
|
|
+ if (empty($targetIds)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $updates = array_filter([
|
|
|
+ 'edition' => $this->batch['edition'] ?? null,
|
|
|
+ 'grade' => $this->batch['grade'] ?? null,
|
|
|
+ 'term' => $this->batch['term'] ?? null,
|
|
|
+ 'chapter' => $this->batch['chapter'] ?? null,
|
|
|
+ 'source_type' => $this->batch['source_type'] ?? null,
|
|
|
+ 'source_year' => $this->batch['source_year'] ?? null,
|
|
|
+ 'textbook_id' => $this->batch['textbook_id'] ?? null,
|
|
|
+ 'series_id' => $this->batch['textbook_series_id'] ?? null,
|
|
|
+ ], fn ($value) => $value !== null && $value !== '');
|
|
|
+
|
|
|
+ foreach (SourcePaper::query()->whereIn('id', $targetIds)->get() as $paper) {
|
|
|
+ $meta = $paper->meta ?? [];
|
|
|
+
|
|
|
+ if (!empty($this->batch['source_name'])) {
|
|
|
+ $meta['source_name'] = $this->batch['source_name'];
|
|
|
+ }
|
|
|
+ if (!empty($this->batch['source_page'])) {
|
|
|
+ $meta['source_page'] = $this->batch['source_page'];
|
|
|
+ }
|
|
|
+ if (!empty($this->batch['tags'])) {
|
|
|
+ $meta['tags'] = $this->explodeTags($this->batch['tags']);
|
|
|
+ }
|
|
|
+ if (!empty($this->batch['bundle_key'])) {
|
|
|
+ $meta['bundle_key'] = $this->batch['bundle_key'];
|
|
|
+ }
|
|
|
+ if (!empty($this->batch['expected_count'])) {
|
|
|
+ $meta['expected_count'] = $this->batch['expected_count'];
|
|
|
+ }
|
|
|
+ if (!empty($this->batch['catalog_node_ids'])) {
|
|
|
+ $catalogNodeIds = $this->normalizeCatalogNodeIds($this->batch['catalog_node_ids']);
|
|
|
+ $catalogNodeIds = $this->filterCatalogNodeIdsForTextbook($updates['textbook_id'] ?? $paper->textbook_id, $catalogNodeIds);
|
|
|
+ $meta['catalog_node_ids'] = $catalogNodeIds;
|
|
|
+ $meta['catalog_node_id'] = $catalogNodeIds[0] ?? null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $paper->update(array_merge($updates, ['meta' => $meta]));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function isBatchEmpty(): bool
|
|
|
+ {
|
|
|
+ $fields = [
|
|
|
+ 'edition', 'grade', 'term', 'chapter', 'source_type',
|
|
|
+ 'source_year', 'textbook_id', 'textbook_series_id',
|
|
|
+ 'source_name', 'source_page', 'tags', 'bundle_key',
|
|
|
+ 'expected_count',
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($fields as $field) {
|
|
|
+ if (!empty($this->batch[$field])) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return empty($this->batch['catalog_node_ids']);
|
|
|
+ }
|
|
|
}
|