mailto = trim((string)$config['mailto']); } else { $this->mailto = trim((string)Env::get('crossref_mailto', Env::get('pubmed_email', ''))); } $this->yiigleApiBase = rtrim(trim((string)($config['yiigle_api_base'] ?? Env::get('yiigle_api_base', ''))), '/'); $this->yiigleApiKey = trim((string)($config['yiigle_api_key'] ?? Env::get('yiigle_api_key', ''))); $this->skipYiigle = !empty($config['skip_yiigle']); } public static function isCmaJournalDoi($doi) { $doi = strtolower(trim((string)$doi)); return $doi !== '' && strpos($doi, 'cma.j.cn') !== false; } /** * @return array{ * title:string,journal:string,year:string,abstract:string,content:string, * pmid:string,blocks:array,sources:array,fetch_log:string * }|null */ public function fetchByDoi($doi) { $doi = $this->normalizeDoi($doi); if ($doi === '' || !self::isCmaJournalDoi($doi)) { return null; } $cacheKey = 'cma_lit_' . sha1(strtolower($doi)); $cached = $this->cacheGet($cacheKey, 7 * 86400); if (is_array($cached)) { return $cached; } $blocks = []; $sources = []; $abstract = ''; $content = ''; $title = ''; $journal = ''; $year = ''; $pmid = ''; $logs = []; if (!$this->skipYiigle && $this->yiigleApiBase !== '' && $this->yiigleApiKey !== '') { $yiigle = $this->fetchFromYiigleApi($doi); if (is_array($yiigle)) { $sources[] = 'cma_yiigle'; $title = $this->pickNonEmpty($title, $yiigle['title'] ?? ''); $journal = $this->pickNonEmpty($journal, $yiigle['journal'] ?? ''); $year = $this->pickNonEmpty($year, $yiigle['year'] ?? ''); if (trim((string)($yiigle['abstract'] ?? '')) !== '') { $abstract = trim((string)$yiigle['abstract']); } if (trim((string)($yiigle['content'] ?? '')) !== '') { $content = trim((string)$yiigle['content']); } if (!empty($yiigle['block'])) { $blocks[] = (string)$yiigle['block']; } $logs[] = 'yiigle=' . ($yiigle['status'] ?? 'ok'); } } $openAlex = $this->fetchFromOpenAlex($doi); if (is_array($openAlex)) { $sources[] = 'cma_openalex'; $title = $this->pickNonEmpty($title, $openAlex['title'] ?? ''); $journal = $this->pickNonEmpty($journal, $openAlex['journal'] ?? ''); $year = $this->pickNonEmpty($year, $openAlex['year'] ?? ''); $pmid = $this->pickNonEmpty($pmid, $openAlex['pmid'] ?? ''); if ($abstract === '' && trim((string)($openAlex['abstract'] ?? '')) !== '') { $abstract = trim((string)$openAlex['abstract']); } if (!empty($openAlex['block'])) { $blocks[] = (string)$openAlex['block']; } $logs[] = 'openalex=' . ($abstract !== '' ? 'abstract' : 'meta'); } if ($abstract === '' && $content === '' && empty($blocks)) { return null; } $result = [ 'title' => $title, 'journal' => $journal, 'year' => $year, 'abstract' => $abstract, 'content' => $content, 'pmid' => $pmid, 'blocks' => array_values(array_filter($blocks)), 'sources' => array_values(array_unique($sources)), 'fetch_log' => 'cma_doi=' . $doi . '; ' . implode('; ', $logs), ]; $this->cacheSet($cacheKey, $result); return $result; } private function fetchFromOpenAlex($doi) { $url = 'https://api.openalex.org/works/https://doi.org/' . rawurlencode($doi); if ($this->mailto !== '') { $url .= '?mailto=' . rawurlencode($this->mailto); } $raw = $this->httpGet($url); $json = json_decode((string)$raw, true); if (!is_array($json)) { return null; } $title = trim((string)($json['title'] ?? $json['display_name'] ?? '')); $journal = trim((string)($json['primary_location']['raw_source_name'] ?? '')); if ($journal === '') { $journal = trim((string)($json['primary_location']['source']['display_name'] ?? '')); } if (strcasecmp($journal, 'PubMed') === 0) { $journal = ''; } $year = trim((string)($json['publication_year'] ?? '')); $abstract = $this->reconstructOpenAlexAbstract($json['abstract_inverted_index'] ?? null); if ($abstract === '') { $abstract = trim((string)($json['abstract'] ?? '')); } $pmid = ''; $pmidRaw = (string)($json['ids']['pmid'] ?? ''); if (preg_match('/(\d+)/', $pmidRaw, $m)) { $pmid = $m[1]; } $lines = ['=== 中华医学期刊 / OpenAlex (DOI ' . $doi . ') ===']; if ($title !== '') { $lines[] = 'Title: ' . $title; } if ($journal !== '') { $lines[] = 'Journal: ' . $journal; } if ($year !== '') { $lines[] = 'Year: ' . $year; } if ($abstract !== '') { $lines[] = 'Abstract: ' . $abstract; } return [ 'title' => $title, 'journal' => $journal, 'year' => $year, 'abstract' => $abstract, 'pmid' => $pmid, 'block' => implode("\n", $lines), ]; } /** * 可选:机构购买的 Yiigle 资源 API(未配置则跳过)。 */ private function fetchFromYiigleApi($doi) { $url = $this->yiigleApiBase . '/resource/queryByDoi?doi=' . rawurlencode($doi); $raw = $this->httpGet($url, [ 'Authorization: Bearer ' . $this->yiigleApiKey, 'Accept: application/json', ]); $json = json_decode((string)$raw, true); if (!is_array($json)) { return ['status' => 'empty']; } $data = $json['data'] ?? $json; if (!is_array($data)) { return ['status' => 'invalid']; } $abstract = trim((string)($data['abstract'] ?? $data['summary'] ?? '')); $content = trim((string)($data['content'] ?? $data['fullText'] ?? '')); $title = trim((string)($data['title'] ?? '')); $journal = trim((string)($data['journal'] ?? $data['journalName'] ?? '')); $year = trim((string)($data['year'] ?? $data['pubYear'] ?? '')); $block = ''; if ($abstract !== '' || $content !== '') { $lines = ['=== 中华医学期刊网 Yiigle (DOI ' . $doi . ') ===']; if ($title !== '') { $lines[] = 'Title: ' . $title; } if ($abstract !== '') { $lines[] = 'Abstract: ' . $abstract; } if ($content !== '') { $lines[] = 'Content: ' . mb_substr($content, 0, 8000); } $block = implode("\n", $lines); } return [ 'status' => ($abstract !== '' || $content !== '') ? 'ok' : 'no_text', 'title' => $title, 'journal' => $journal, 'year' => $year, 'abstract' => $abstract, 'content' => $content, 'block' => $block, ]; } private function reconstructOpenAlexAbstract($invertedIndex) { if (!is_array($invertedIndex) || empty($invertedIndex)) { return ''; } $tokens = []; foreach ($invertedIndex as $token => $positions) { if (!is_array($positions)) { continue; } foreach ($positions as $pos) { $tokens[intval($pos)] = (string)$token; } } if (empty($tokens)) { return ''; } ksort($tokens, SORT_NUMERIC); return trim(implode(' ', $tokens)); } private function normalizeDoi($doi) { $doi = preg_replace('#^https?://(dx\.)?doi\.org/#i', '', trim((string)$doi)); return trim($doi, " \t\n\r\0\x0B/"); } private function pickNonEmpty($current, $candidate) { $current = trim((string)$current); $candidate = trim((string)$candidate); if ($current !== '') { return $current; } return $candidate; } private function httpGet($url, array $headers = []) { $headers = array_merge([ 'User-Agent: TMRjournals-CmaJournal/1.0', 'Accept: application/json, text/plain, */*', ], $headers); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_HTTPHEADER => $headers, ]); $body = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code < 200 || $code >= 300) { return ''; } return is_string($body) ? $body : ''; } private function cacheDir() { return rtrim(ROOT_PATH, '/') . '/runtime/cma_journal_cache'; } private function cacheGet($key, $ttlSeconds) { $file = $this->cacheDir() . '/' . $key . '.json'; if (!is_file($file)) { return null; } $mtime = filemtime($file); if (!$mtime || (time() - $mtime) > $ttlSeconds) { return null; } $decoded = json_decode((string)@file_get_contents($file), true); return is_array($decoded) ? $decoded : null; } private function cacheSet($key, array $value) { $dir = $this->cacheDir(); if (!is_dir($dir)) { @mkdir($dir, 0777, true); } @file_put_contents($dir . '/' . $key . '.json', json_encode($value, JSON_UNESCAPED_UNICODE)); } }