pubmed = new PubmedService(); $this->crossref = new CrossrefService([ 'mailto' => trim((string)Env::get('crossref_mailto', '')), ]); $this->classifier = new ReferenceTypeClassifier(['use_llm' => false]); } /** * 按 DOI 聚合元数据 * * @return array|null 两个源都查不到时返回 null */ public function fetchByDoi($doi) { $doi = $this->normalizeDoi($doi); if ($doi === '') { return null; } $pub = null; try { $pub = $this->pubmed->fetchByDoi($doi); } catch (\Throwable $e) { $pub = null; } if (!is_array($pub)) { $pub = null; } $cr = $this->crossref->fetchWorkSummary($doi); if (!is_array($cr)) { $cr = null; } if ($pub === null && $cr === null) { return null; } $sources = []; if ($pub !== null) $sources[] = 'pubmed'; if ($cr !== null) $sources[] = 'crossref'; $title = $this->pickTitle($pub, $cr); $joura = $this->pickJournal($pub, $cr); $author = $this->pickAuthor($pub, $cr); $dateno = $this->pickDateno($pub, $cr); $doilink = trim((string)($cr['doilink'] ?? '')); if ($doilink === '') { $doilink = 'https://doi.org/' . $doi; } $crossrefType = trim((string)($cr['raw']['type'] ?? '')); $type = $this->pickType($pub, $crossrefType); $retract = $this->pickRetraction($pub, $cr); return [ 'doi' => $doi, 'pmid' => trim((string)($pub['pmid'] ?? '')), 'title' => $title, 'author' => $author, 'joura' => $joura, 'dateno' => $dateno, 'doilink' => $doilink, 'type' => $type, 'crossref_type' => $crossrefType, 'is_retracted' => $retract['is_retracted'], 'retract_reason' => $retract['reason'], 'has_cjk' => $this->crossref->hasCjk($title) || $this->crossref->hasCjk($joura) || $this->crossref->hasCjk($author), 'sources' => $sources, 'crossref_summary' => $cr, 'pubmed' => $pub, ]; } /** * 标题:PubMed 优先。非英文原文在 PubMed 里是括号包裹的英译标题,去掉括号更贴合排版。 */ private function pickTitle($pub, $cr) { $title = trim((string)($pub['title'] ?? '')); if ($title !== '') { $title = rtrim($title, '.'); if (preg_match('/^\[(.+)\]$/s', $title, $m)) { $title = trim($m[1]); } return $title; } return trim((string)($cr['title'] ?? '')); } /** * 期刊名:PubMed 的规范缩写优先,其次 Crossref */ private function pickJournal($pub, $cr) { $abbr = trim((string)($pub['journal_iso_abbr'] ?? '')); if ($abbr === '') { $abbr = trim((string)($pub['journal_medline_ta'] ?? '')); } if ($abbr !== '') { return $abbr; } return trim((string)($cr['joura'] ?? '')); } private function pickAuthor($pub, $cr) { $authors = isset($pub['authors']) && is_array($pub['authors']) ? $pub['authors'] : []; if (!empty($authors)) { $citation = $this->pubmed->authorsCitation($authors); if ($citation !== '') { return $citation; } } return $this->crossref->getAuthorsCitation($cr['raw'] ?? []); } /** * 卷期页:PubMed 有卷或页时以 PubMed 为准,否则用 Crossref * * 格式与 citation.doi.org 路径一致:Year;Volume(Issue):Pages */ private function pickDateno($pub, $cr) { $volume = trim((string)($pub['volume'] ?? '')); $pages = $this->expandPageRange((string)($pub['pages'] ?? '')); if ($volume === '' && $pages === '') { return trim((string)($cr['dateno'] ?? '')); } if ($volume !== '') { $issue = trim((string)($pub['issue'] ?? '')); $volume .= $issue !== '' ? "({$issue})" : ''; } $tail = $volume; if ($pages !== '') { $tail = $tail !== '' ? $tail . ':' . $pages : $pages; } $year = trim((string)($pub['year'] ?? '')); if ($year === '' || $tail === '') { return $year !== '' ? $year : $tail; } return $year . ';' . $tail; } /** * 展开 NLM 缩写式尾页:210-8 表示 210-218 */ private function expandPageRange($pages) { $pages = trim((string)$pages); if ($pages === '' || strpos($pages, '-') === false) { return $pages; } $parts = explode('-', $pages, 2); $start = trim($parts[0]); $end = trim($parts[1]); if ($start === '' || $end === '') { return $pages; } if (ctype_digit($start) && ctype_digit($end) && strlen($end) < strlen($start)) { $end = substr($start, 0, strlen($start) - strlen($end)) . $end; } return $start . '-' . $end; } /** * 类型:Crossref 的 type 最权威,PubMed 的 publication_types 兜底 */ private function pickType($pub, $crossrefType) { $mapped = $this->classifier->mapCrossrefType($crossrefType); if ($mapped !== '') { return $mapped; } $types = isset($pub['publication_types']) && is_array($pub['publication_types']) ? $pub['publication_types'] : []; foreach ($types as $t) { $t = strtolower(trim((string)$t)); if ($t === '') { continue; } if (strpos($t, 'congress') !== false) { return ReferenceTypeClassifier::TYPE_CONFERENCE; } if (strpos($t, 'journal article') !== false || strpos($t, 'review') !== false || strpos($t, 'clinical trial') !== false || strpos($t, 'meta-analysis') !== false || strpos($t, 'case reports') !== false || strpos($t, 'observational study') !== false || strpos($t, 'comparative study') !== false || strpos($t, 'multicenter study') !== false || strpos($t, 'editorial') !== false || strpos($t, 'letter') !== false) { return ReferenceTypeClassifier::TYPE_JOURNAL; } } return ''; } private function pickRetraction($pub, $cr) { $types = isset($pub['publication_types']) && is_array($pub['publication_types']) ? $pub['publication_types'] : []; foreach ($types as $t) { if (stripos((string)$t, 'retract') !== false) { return ['is_retracted' => 1, 'reason' => 'PubMed 标记:' . trim((string)$t)]; } } return [ 'is_retracted' => !empty($cr['is_retracted']) ? 1 : 0, 'reason' => (string)($cr['retract_reason'] ?? ''), ]; } public function normalizeDoi($doi) { $doi = preg_replace('#^https?://(dx\.)?doi\.org/#i', '', trim((string)$doi)); return trim($doi, " \t\n\r\0\x0B/"); } }