参考文献type

This commit is contained in:
wangjinlei
2026-07-09 15:42:55 +08:00
parent 4fd967b1d4
commit 4cf26ea682
4 changed files with 278 additions and 16 deletions

View File

@@ -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([]);
}

View File

@@ -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)]);
}

View File

@@ -0,0 +1,251 @@
<?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 约定一致)
* - otherrefer_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/");
}
}

View File

@@ -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));