Files
tougao/application/common/CmaJournalLiteratureService.php
wyn 8785610e6d 参考文献作者堆叠
参考文献相关性检测
作者ai写作辅助检测工作
2026-07-15 10:49:05 +08:00

330 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common;
use think\Env;
/**
* 中华医学会期刊 DOIcma.j.cn文献抓取优先中文数据源。
*
* 1. 可选Yiigle 机构资源 API需配置 yiigle_api_base + yiigle_api_key
* 2. OpenAlex常含中文摘要abstract_inverted_index中华期刊在 PubMed 无摘要时仍可用
*/
class CmaJournalLiteratureService
{
private $timeout = 20;
private $mailto = '';
private $yiigleApiBase = '';
private $yiigleApiKey = '';
private $skipYiigle = false;
public function __construct(array $config = [])
{
if (isset($config['mailto'])) {
$this->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));
}
}