252 lines
8.3 KiB
PHP
252 lines
8.3 KiB
PHP
<?php
|
||
|
||
namespace app\common;
|
||
|
||
use think\Db;
|
||
use think\Env;
|
||
use think\Queue;
|
||
|
||
/**
|
||
* 参考文献分流处理:freshRefers 入口之后,按 journal / book / other 三路处理。
|
||
*
|
||
* - journal:有 DOI 入 ArticleReferDetailQueue;无 DOI 保留原文
|
||
* - book:填充结构化字段 author/title/joura/dateno/isbn(与 Preaccept、References 约定一致)
|
||
* - other:refer_frag = refer_content,不做结构化
|
||
*/
|
||
class ReferenceDispatchService
|
||
{
|
||
/** @var ReferenceTypeClassifier */
|
||
private $classifier;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->classifier = new ReferenceTypeClassifier(['use_llm' => true]);
|
||
}
|
||
|
||
/**
|
||
* 对某篇生产文章的全部参考文献按类型分流处理
|
||
*/
|
||
public function dispatchRefersByType($pArticleId)
|
||
{
|
||
$pArticleId = intval($pArticleId);
|
||
if ($pArticleId <= 0) {
|
||
return;
|
||
}
|
||
|
||
$refers = Db::name('production_article_refer')
|
||
->where('p_article_id', $pArticleId)
|
||
->where('state', 0)
|
||
->order('index asc, p_refer_id asc')
|
||
->select();
|
||
|
||
if (empty($refers)) {
|
||
return;
|
||
}
|
||
|
||
$crossref = new CrossrefService([
|
||
'mailto' => trim((string)Env::get('crossref_mailto', '')),
|
||
]);
|
||
|
||
foreach ($refers as $refer) {
|
||
$summary = null;
|
||
$crossrefType = '';
|
||
if (trim((string)$refer['refer_doi']) !== '') {
|
||
$doiNorm = $this->normalizeDoi($refer['refer_doi']);
|
||
if ($doiNorm !== '') {
|
||
$summary = $crossref->fetchWorkSummary($doiNorm);
|
||
if ($summary && !empty($summary['raw']['type'])) {
|
||
$crossrefType = (string)$summary['raw']['type'];
|
||
}
|
||
}
|
||
}
|
||
|
||
$typeInfo = $this->classifier->classify((string)$refer['refer_content'], $crossrefType);
|
||
$dispatchType = $this->classifier->normalizeDispatchType($typeInfo['type']);
|
||
|
||
Db::name('production_article_refer')->where('p_refer_id', $refer['p_refer_id'])->update([
|
||
'refer_type' => $dispatchType,
|
||
'update_time' => time(),
|
||
]);
|
||
$refer['refer_type'] = $dispatchType;
|
||
|
||
switch ($dispatchType) {
|
||
case ReferenceTypeClassifier::TYPE_BOOK:
|
||
$this->processBookRefer($refer, $summary, $crossref);
|
||
break;
|
||
case ReferenceTypeClassifier::TYPE_OTHER:
|
||
$this->processOtherRefer($refer);
|
||
break;
|
||
default:
|
||
$this->processJournalRefer($refer);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private function processJournalRefer(array $refer)
|
||
{
|
||
$pReferId = intval($refer['p_refer_id']);
|
||
if (trim((string)$refer['refer_doi']) === '') {
|
||
Db::name('production_article_refer')->where('p_refer_id', $pReferId)->update([
|
||
'refer_frag' => $refer['refer_content'],
|
||
'refer_type' => ReferenceTypeClassifier::TYPE_JOURNAL,
|
||
'is_deal' => 1,
|
||
'update_time' => time(),
|
||
]);
|
||
return;
|
||
}
|
||
|
||
Queue::push('app\api\job\ArticleReferDetailQueue@fire', $refer, 'ArticleReferDetailQueue');
|
||
}
|
||
|
||
private function processOtherRefer(array $refer)
|
||
{
|
||
Db::name('production_article_refer')->where('p_refer_id', intval($refer['p_refer_id']))->update([
|
||
'refer_frag' => $refer['refer_content'],
|
||
'refer_type' => ReferenceTypeClassifier::TYPE_OTHER,
|
||
'cs' => 0,
|
||
'is_deal' => 1,
|
||
'update_time' => time(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* book:结构化字段,DOI 仅用于补数据
|
||
*/
|
||
private function processBookRefer(array $refer, $summary, CrossrefService $crossref)
|
||
{
|
||
$pReferId = intval($refer['p_refer_id']);
|
||
$content = (string)$refer['refer_content'];
|
||
$update = [
|
||
'refer_type' => ReferenceTypeClassifier::TYPE_BOOK,
|
||
'is_deal' => 1,
|
||
'update_time' => time(),
|
||
];
|
||
|
||
if (is_array($summary) && !empty($summary['raw'])) {
|
||
$raw = $summary['raw'];
|
||
$authorCitation = $crossref->getAuthorsCitation($raw, 3);
|
||
$update['author'] = $authorCitation !== '' ? rtrim($authorCitation, '.') . '.' : '';
|
||
$update['title'] = trim((string)($summary['title'] ?? ''));
|
||
$update['joura'] = $this->extractBookPublisher($raw, $summary);
|
||
$update['dateno'] = $this->extractBookDateno($raw);
|
||
$isbn = $this->extractIsbnFromRaw($raw);
|
||
if ($isbn === '' && !empty($refer['refer_doi'])) {
|
||
$doi = $this->normalizeDoi($refer['refer_doi']);
|
||
$isbn = $doi !== '' ? 'https://doi.org/' . $doi : '';
|
||
}
|
||
$update['isbn'] = $isbn;
|
||
$update['is_ja'] = 1;
|
||
} else {
|
||
$parsed = $this->parseBookFromContent($content);
|
||
$update = array_merge($update, $parsed);
|
||
}
|
||
|
||
$hasCore = trim((string)($update['author'] ?? '')) !== ''
|
||
&& trim((string)($update['title'] ?? '')) !== '';
|
||
$update['cs'] = $hasCore ? 1 : 0;
|
||
if (!$hasCore) {
|
||
$update['refer_frag'] = $content;
|
||
}
|
||
|
||
Db::name('production_article_refer')->where('p_refer_id', $pReferId)->update($update);
|
||
}
|
||
|
||
private function extractBookPublisher(array $raw, array $summary)
|
||
{
|
||
$publisher = trim((string)($raw['publisher'] ?? ''));
|
||
if ($publisher !== '') {
|
||
return $publisher;
|
||
}
|
||
$pub = $summary['publisher'] ?? [];
|
||
if (!empty($pub['publisher'])) {
|
||
return trim((string)$pub['publisher']);
|
||
}
|
||
if (!empty($pub['title'])) {
|
||
return trim((string)$pub['title']);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
private function extractBookDateno(array $raw)
|
||
{
|
||
if (!empty($raw['published']['date-parts'][0][0])) {
|
||
return (string)$raw['published']['date-parts'][0][0];
|
||
}
|
||
if (!empty($raw['issued']['date-parts'][0][0])) {
|
||
return (string)$raw['issued']['date-parts'][0][0];
|
||
}
|
||
if (!empty($raw['created']['date-parts'][0][0])) {
|
||
return (string)$raw['created']['date-parts'][0][0];
|
||
}
|
||
return '';
|
||
}
|
||
|
||
private function extractIsbnFromRaw(array $raw)
|
||
{
|
||
if (empty($raw['ISBN']) || !is_array($raw['ISBN'])) {
|
||
return '';
|
||
}
|
||
foreach ($raw['ISBN'] as $isbn) {
|
||
$isbn = trim((string)$isbn);
|
||
if ($isbn !== '') {
|
||
return $isbn;
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 无 Crossref 时从原文尽量抽取 book 结构化字段
|
||
*/
|
||
private function parseBookFromContent($content)
|
||
{
|
||
$content = trim((string)$content);
|
||
$out = [
|
||
'author' => '',
|
||
'title' => '',
|
||
'joura' => '',
|
||
'dateno' => '',
|
||
'isbn' => '',
|
||
'is_ja' => 1,
|
||
];
|
||
|
||
if ($content === '') {
|
||
return $out;
|
||
}
|
||
|
||
if (preg_match('/\bISBN[:\s]*([\d\-Xx\s]+)/i', $content, $m)) {
|
||
$out['isbn'] = preg_replace('/\s+/', '-', trim($m[1]));
|
||
}
|
||
|
||
if (preg_match('/\b(19|20)\d{2}\b/', $content, $m)) {
|
||
$out['dateno'] = $m[0];
|
||
}
|
||
|
||
// Place: Publisher; Year → joura 取 Publisher
|
||
if (preg_match('/:\s*([^;]+);\s*(19|20)\d{2}/', $content, $m)) {
|
||
$out['joura'] = trim($m[1]);
|
||
} elseif (preg_match('/\b([A-Z][A-Za-z .&]+(?:Press|Publishing|Publisher|Books?))\b/i', $content, $m)) {
|
||
$out['joura'] = trim($m[1]);
|
||
}
|
||
|
||
// 作者. 标题. ... 简单拆分
|
||
$parts = preg_split('/\.\s+/', $content, 3);
|
||
if (is_array($parts) && count($parts) >= 2) {
|
||
$out['author'] = trim($parts[0]);
|
||
if (substr($out['author'], -1) !== '.') {
|
||
$out['author'] .= '.';
|
||
}
|
||
$out['title'] = trim(rtrim($parts[1], '.'));
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
private function normalizeDoi($doi)
|
||
{
|
||
$doi = preg_replace('#^https?://(dx\.)?doi\.org/#i', '', trim((string)$doi));
|
||
return trim($doi, " \t\n\r\0\x0B/");
|
||
}
|
||
}
|