参考文献作者堆叠

参考文献相关性检测
作者ai写作辅助检测工作
This commit is contained in:
wyn
2026-07-15 10:49:05 +08:00
parent da71dfc04e
commit 8785610e6d
27 changed files with 10008 additions and 204 deletions

View File

@@ -0,0 +1,329 @@
<?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));
}
}