参考文献格式

This commit is contained in:
wangjinlei
2026-08-05 18:36:17 +08:00
parent 2db6f170fe
commit d72b16d9e8
7 changed files with 563 additions and 112 deletions

View File

@@ -0,0 +1,263 @@
<?php
namespace app\common;
use think\Env;
/**
* 参考文献元数据聚合PubMed 优先Crossref 补全。
*
* 医学期刊的著录信息以 PubMedNLM 人工校订)更贴合期次著录:
* - 年份PubMed 取期次年Crossref 的 issued 是线上/线下较早者,在线优先出版会偏早一年
* - 期刊名PubMed 的 ISOAbbreviation / MedlineTA 就是排版要的规范缩写
* - 卷期页PubMed 覆盖完整Crossref 在 ahead-of-print 阶段常缺失
*
* Crossref 负责非 MEDLINE 收录文献的兜底,并提供 ORCID 作者明细与撤稿关系。
*/
class ReferenceMetadataService
{
/** @var PubmedService */
private $pubmed;
/** @var CrossrefService */
private $crossref;
/** @var ReferenceTypeClassifier */
private $classifier;
public function __construct()
{
$this->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, 3);
if ($citation !== '') {
return $citation;
}
}
return $this->crossref->getAuthorsCitation($cr['raw'] ?? [], 3);
}
/**
* 卷期页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/");
}
}