classifier = new ReferenceTypeClassifier(['use_llm' => true]); } /** * 将文章下全部有效参考文献逐条入队分流(HTTP 内只做 push,立即返回) */ public function enqueueRefersByType($pArticleId) { $pArticleId = intval($pArticleId); if ($pArticleId <= 0) { return; } $refers = Db::name('production_article_refer') ->where('p_article_id', $pArticleId) ->where('state', 0) ->order('index asc, p_refer_id asc') ->select(); if (empty($refers)) { return; } foreach ($refers as $refer) { Queue::push( 'app\api\job\ReferenceDispatchQueue@fire', $refer, 'ReferenceDispatchQueue' ); } } /** * 处理单条参考文献分流(由 ReferenceDispatchQueue 调用) */ public function dispatchReferByType($pReferId) { $pReferId = intval($pReferId); if ($pReferId <= 0) { return; } $refer = Db::name('production_article_refer') ->where('p_refer_id', $pReferId) ->where('state', 0) ->find(); if (empty($refer)) { return; } $crossref = new CrossrefService([ 'mailto' => trim((string)Env::get('crossref_mailto', '')), ]); $summary = null; $crossrefType = ''; if (trim((string)$refer['refer_doi']) !== '') { $doiNorm = $this->normalizeDoi($refer['refer_doi']); if ($doiNorm !== '') { $summary = $crossref->fetchWorkSummary($doiNorm); if ($summary && !empty($summary['raw']['type'])) { $crossrefType = (string)$summary['raw']['type']; } } } $typeInfo = $this->classifier->classify((string)$refer['refer_content'], $crossrefType); $dispatchType = $this->classifier->normalizeDispatchType($typeInfo['type']); Db::name('production_article_refer')->where('p_refer_id', $refer['p_refer_id'])->update([ 'refer_type' => $dispatchType, 'update_time' => time(), ]); $refer['refer_type'] = $dispatchType; switch ($dispatchType) { case ReferenceTypeClassifier::TYPE_BOOK: $this->processBookRefer($refer, $summary, $crossref); break; case ReferenceTypeClassifier::TYPE_OTHER: $this->processOtherRefer($refer); break; default: $this->processJournalRefer($refer); break; } } /** * 同步处理整篇文章(仅供脚本/调试,勿在 HTTP 或单条队列任务中批量调用) */ public function dispatchRefersByType($pArticleId) { $pArticleId = intval($pArticleId); if ($pArticleId <= 0) { return; } $refers = Db::name('production_article_refer') ->where('p_article_id', $pArticleId) ->where('state', 0) ->order('index asc, p_refer_id asc') ->column('p_refer_id'); foreach ($refers as $pReferId) { $this->dispatchReferByType($pReferId); } } private function processJournalRefer(array $refer) { $pReferId = intval($refer['p_refer_id']); if (trim((string)$refer['refer_doi']) === '') { Db::name('production_article_refer')->where('p_refer_id', $pReferId)->update([ 'refer_frag' => $refer['refer_content'], 'refer_type' => ReferenceTypeClassifier::TYPE_JOURNAL, 'is_deal' => 1, 'update_time' => time(), ]); return; } Queue::push('app\api\job\ArticleReferDetailQueue@fire', $refer, 'ArticleReferDetailQueue'); } private function processOtherRefer(array $refer) { Db::name('production_article_refer')->where('p_refer_id', intval($refer['p_refer_id']))->update([ 'refer_frag' => $refer['refer_content'], 'refer_type' => ReferenceTypeClassifier::TYPE_OTHER, 'cs' => 0, 'is_deal' => 1, 'update_time' => time(), ]); } /** * book:结构化字段,DOI 仅用于补数据 */ private function processBookRefer(array $refer, $summary, CrossrefService $crossref) { $pReferId = intval($refer['p_refer_id']); $content = (string)$refer['refer_content']; $update = [ 'refer_type' => ReferenceTypeClassifier::TYPE_BOOK, 'is_deal' => 1, 'update_time' => time(), ]; if (is_array($summary) && !empty($summary['raw'])) { $raw = $summary['raw']; $authorCitation = $crossref->getAuthorsCitation($raw, 3); $update['author'] = $authorCitation !== '' ? rtrim($authorCitation, '.') . '.' : ''; $update['title'] = trim((string)($summary['title'] ?? '')); $update['joura'] = $this->extractBookPublisher($raw, $summary); $update['dateno'] = $this->extractBookDateno($raw); $isbn = $this->extractIsbnFromRaw($raw); if ($isbn === '' && !empty($refer['refer_doi'])) { $doi = $this->normalizeDoi($refer['refer_doi']); $isbn = $doi !== '' ? 'https://doi.org/' . $doi : ''; } $update['isbn'] = $isbn; $update['is_ja'] = 1; } else { $parsed = $this->parseBookFromContent($content); $update = array_merge($update, $parsed); } $hasCore = trim((string)($update['author'] ?? '')) !== '' && trim((string)($update['title'] ?? '')) !== ''; $update['cs'] = $hasCore ? 1 : 0; if (!$hasCore) { $update['refer_frag'] = $content; } Db::name('production_article_refer')->where('p_refer_id', $pReferId)->update($update); } private function extractBookPublisher(array $raw, array $summary) { $publisher = trim((string)($raw['publisher'] ?? '')); if ($publisher !== '') { return $publisher; } $pub = $summary['publisher'] ?? []; if (!empty($pub['publisher'])) { return trim((string)$pub['publisher']); } if (!empty($pub['title'])) { return trim((string)$pub['title']); } return ''; } private function extractBookDateno(array $raw) { if (!empty($raw['published']['date-parts'][0][0])) { return (string)$raw['published']['date-parts'][0][0]; } if (!empty($raw['issued']['date-parts'][0][0])) { return (string)$raw['issued']['date-parts'][0][0]; } if (!empty($raw['created']['date-parts'][0][0])) { return (string)$raw['created']['date-parts'][0][0]; } return ''; } private function extractIsbnFromRaw(array $raw) { if (empty($raw['ISBN']) || !is_array($raw['ISBN'])) { return ''; } foreach ($raw['ISBN'] as $isbn) { $isbn = trim((string)$isbn); if ($isbn !== '') { return $isbn; } } return ''; } /** * 无 Crossref 时从原文尽量抽取 book 结构化字段 */ private function parseBookFromContent($content) { $content = trim((string)$content); $out = [ 'author' => '', 'title' => '', 'joura' => '', 'dateno' => '', 'isbn' => '', 'is_ja' => 1, ]; if ($content === '') { return $out; } if (preg_match('/\bISBN[:\s]*([\d\-Xx\s]+)/i', $content, $m)) { $out['isbn'] = preg_replace('/\s+/', '-', trim($m[1])); } if (preg_match('/\b(19|20)\d{2}\b/', $content, $m)) { $out['dateno'] = $m[0]; } // Place: Publisher; Year → joura 取 Publisher if (preg_match('/:\s*([^;]+);\s*(19|20)\d{2}/', $content, $m)) { $out['joura'] = trim($m[1]); } elseif (preg_match('/\b([A-Z][A-Za-z .&]+(?:Press|Publishing|Publisher|Books?))\b/i', $content, $m)) { $out['joura'] = trim($m[1]); } // 作者. 标题. ... 简单拆分 $parts = preg_split('/\.\s+/', $content, 3); if (is_array($parts) && count($parts) >= 2) { $out['author'] = trim($parts[0]); if (substr($out['author'], -1) !== '.') { $out['author'] .= '.'; } $out['title'] = trim(rtrim($parts[1], '.')); } return $out; } private function normalizeDoi($doi) { $doi = preg_replace('#^https?://(dx\.)?doi\.org/#i', '', trim((string)$doi)); return trim($doi, " \t\n\r\0\x0B/"); } }