参考文献作者堆叠
参考文献相关性检测 作者ai写作辅助检测工作
This commit is contained in:
329
application/common/CmaJournalLiteratureService.php
Normal file
329
application/common/CmaJournalLiteratureService.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
use think\Env;
|
||||
|
||||
/**
|
||||
* 中华医学会期刊 DOI(cma.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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user