Files
tougao/application/common/ReferenceDispatchService.php
2026-08-05 18:36:17 +08:00

307 lines
9.9 KiB
PHP
Raw 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\Db;
use think\Queue;
/**
* 参考文献分流处理:按 journal / book / other 三路处理。
* 每条文献单独入 ReferenceDispatchQueue由 worker 并行消费。
*
* - journal有 DOI 入 ArticleReferDetailQueue无 DOI 保留原文
* - book填充结构化字段 author/title/joura/dateno/isbn与 Preaccept、References 约定一致)
* - otherrefer_frag = refer_content不做结构化
*/
class ReferenceDispatchService
{
/** @var ReferenceTypeClassifier */
private $classifier;
/** @var ReferenceMetadataService */
private $metadata;
public function __construct()
{
$this->classifier = new ReferenceTypeClassifier(['use_llm' => true]);
$this->metadata = new ReferenceMetadataService();
}
/**
* 将文章下全部有效参考文献逐条入队分流HTTP 内只做 push立即返回
*/
public function enqueueRefersByType($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;
}
foreach ($refers as $refer) {
Queue::push(
'app\api\job\ReferenceDispatchQueue@fire',
$refer,
'ReferenceDispatchQueue'
);
}
}
/**
* 处理单条参考文献分流(由 ReferenceDispatchQueue 调用)
*/
public function dispatchReferByType($pReferId)
{
$pReferId = intval($pReferId);
if ($pReferId <= 0) {
return;
}
$refer = Db::name('production_article_refer')
->where('p_refer_id', $pReferId)
->where('state', 0)
->find();
if (empty($refer)) {
return;
}
$meta = null;
$typeHint = '';
if (trim((string)$refer['refer_doi']) !== '') {
$doiNorm = $this->normalizeDoi($refer['refer_doi']);
if ($doiNorm !== '') {
$meta = $this->metadata->fetchByDoi($doiNorm);
if (is_array($meta)) {
$typeHint = trim((string)$meta['crossref_type']);
// Crossref 无 type 但 PubMed 收录时,给分类器一个等价的 Crossref type
if ($typeHint === '' && $meta['type'] === ReferenceTypeClassifier::TYPE_JOURNAL) {
$typeHint = 'journal-article';
}
}
}
}
$typeInfo = $this->classifier->classify((string)$refer['refer_content'], $typeHint);
$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, $meta);
break;
case ReferenceTypeClassifier::TYPE_OTHER:
$this->processOtherRefer($refer);
break;
default:
$this->processJournalRefer($refer);
break;
}
}
/**
* 同步处理整篇文章(仅供脚本/调试,勿在 HTTP 或单条队列任务中批量调用)
*/
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')
->column('p_refer_id');
foreach ($refers as $pReferId) {
$this->dispatchReferByType($pReferId);
}
}
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, $meta)
{
$pReferId = intval($refer['p_refer_id']);
$content = (string)$refer['refer_content'];
$update = [
'refer_type' => ReferenceTypeClassifier::TYPE_BOOK,
'is_deal' => 1,
'update_time' => time(),
];
$summary = is_array($meta) ? $meta['crossref_summary'] : null;
$raw = is_array($summary) ? ($summary['raw'] ?? []) : [];
$hasMeta = is_array($meta)
&& (trim((string)$meta['title']) !== '' || trim((string)$meta['author']) !== '');
if ($hasMeta) {
$authorCitation = trim((string)$meta['author']);
$update['author'] = $authorCitation !== '' ? rtrim($authorCitation, '.') . '.' : '';
$update['title'] = trim((string)$meta['title']);
$update['joura'] = !empty($raw) ? $this->extractBookPublisher($raw, $summary) : '';
$update['dateno'] = !empty($raw) ? $this->extractBookDateno($raw) : '';
$isbn = !empty($raw) ? $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/");
}
}