diff --git a/application/api/controller/Production.php b/application/api/controller/Production.php index dbaf23de..f6f1a0c5 100644 --- a/application/api/controller/Production.php +++ b/application/api/controller/Production.php @@ -11,6 +11,7 @@ use think\Queue; use think\Validate; use think\log; use app\common\ArticleSymbolNormalizer; +use app\common\ReferenceDispatchService; /** * @title 公共管理相关 @@ -1931,7 +1932,7 @@ class Production extends Base } $this->referToDoi($data['p_article_id']); - $this->doiTofrag($data['p_article_id']); + (new ReferenceDispatchService())->dispatchRefersByType($data['p_article_id']); return jsonSuccess([]); } @@ -1998,19 +1999,7 @@ class Production extends Base public function doiTofrag($p_article_id) { - $p_info = $this->production_article_obj->where('p_article_id', $p_article_id)->find(); - $refers = $this->production_article_refer_obj->where('p_article_id', $p_info['p_article_id'])->where('state', 0)->select(); - foreach ($refers as $v) { - if ($v['refer_doi'] == '') { - $this->production_article_refer_obj->where('p_refer_id', $v['p_refer_id'])->update(['refer_frag' => $v['refer_content']]); - } else { - - //修改队列兼容对接OPENAI接口 chengxiaoling 20251128 start - // Queue::push('app\api\job\ts@fire1', $v, 'ts'); - Queue::push('app\api\job\ArticleReferDetailQueue@fire', $v, 'ArticleReferDetailQueue'); - //修改队列兼容对接OPENAI接口 chengxiaoling 20251128 end - } - } + (new ReferenceDispatchService())->dispatchRefersByType($p_article_id); return jsonSuccess([]); } diff --git a/application/common/ProductionArticleRefer.php b/application/common/ProductionArticleRefer.php index 7455d77b..be6e1fd7 100644 --- a/application/common/ProductionArticleRefer.php +++ b/application/common/ProductionArticleRefer.php @@ -54,7 +54,7 @@ class ProductionArticleRefer return json_encode(['status' => 1,'msg' => 'Add to reference processing queue']); } /** - * 处理参考文献 + * 处理参考文献(单个) * * @return void */ @@ -71,10 +71,17 @@ class ProductionArticleRefer } //查询未处理过的数据 $aWhere = ['p_refer_id' => $iPReferId,'p_article_id' => $iPArticleId,'state' => 0]; - $aRefer = Db::name('production_article_refer')->field('refer_doi,refer_content')->where($aWhere)->find(); + $aRefer = Db::name('production_article_refer')->field('refer_doi,refer_content,refer_type')->where($aWhere)->find(); if(empty($aRefer)){ return json_encode(array('status' => 2,'msg' => 'No reference records found'.json_encode($aParam))); } + + // 非期刊类型已在分流阶段处理,队列不再走期刊解析 + $referType = strtolower(trim((string)($aRefer['refer_type'] ?? 'journal'))); + if (in_array($referType, ['book', 'other'], true)) { + return json_encode(['status' => 1, 'msg' => 'Skipped non-journal reference']); + } + if(empty($aRefer['refer_doi'])){ return json_encode(['status' => 4,'msg' => 'Reference DOI is empty'.json_encode($aParam)]); } diff --git a/application/common/ReferenceDispatchService.php b/application/common/ReferenceDispatchService.php new file mode 100644 index 00000000..8358f35a --- /dev/null +++ b/application/common/ReferenceDispatchService.php @@ -0,0 +1,251 @@ +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/"); + } +} diff --git a/application/common/ReferenceTypeClassifier.php b/application/common/ReferenceTypeClassifier.php index b4bc3242..9367481a 100644 --- a/application/common/ReferenceTypeClassifier.php +++ b/application/common/ReferenceTypeClassifier.php @@ -81,6 +81,21 @@ class ReferenceTypeClassifier /** * CrossRef type → 内部枚举映射;未命中返回空串 */ + /** + * 将细分类归并为排版用的三类:journal / book / other + */ + public function normalizeDispatchType($type) + { + $type = strtolower(trim((string)$type)); + if ($type === self::TYPE_BOOK) { + return self::TYPE_BOOK; + } + if ($type === self::TYPE_JOURNAL) { + return self::TYPE_JOURNAL; + } + return self::TYPE_OTHER; + } + public function mapCrossrefType($crossrefType) { $t = strtolower(trim((string)$crossrefType));