参考文献格式--book

This commit is contained in:
wangjinlei
2026-08-07 15:26:09 +08:00
parent 32b3233378
commit 175d2edc55
2 changed files with 796 additions and 6 deletions

View File

@@ -0,0 +1,743 @@
<?php
namespace app\common;
/**
* 图书元数据反查:按 书名 + 作者 + 年份 + 版次 检索 ISBN / 出版社 / 出版年。
*
* 背景:绝大多数图书参考文献既没有 DOI拿不到 Crossref 的 ISBN 数组),
* 原文里也不会写 ISBN所以只靠"抽取"永远拿不到,必须按书目信息去反查。
*
* 数据源:
* - Open Library主源免密钥可直连先检索 work再拉 editions 按年份/版次/出版社挑具体版本
* - Google Books备源需 .env 配 google_books_api_key无密钥时该 IP 会被 429
*
* 环境变量:
* - book_isbn_lookup 0 关闭反查,默认开启
* - book_lookup_timeout 单次请求超时秒数,默认 8
* - google_books_api_key 配置后启用 Google Books 备源
*/
class BookMetadataService
{
/** @var int */
private $timeout = 8;
/** @var bool */
private $enabled = true;
/** @var string */
private $googleKey = '';
/** @var int 单条文献反查的总耗时上限(秒),防止队列被慢请求拖住 */
private $budget = 20;
/** @var float */
private $deadline = 0;
public function __construct(array $config = [])
{
$this->enabled = $this->envGet('book_isbn_lookup', '1') !== '0';
$this->googleKey = trim((string)$this->envGet('google_books_api_key', ''));
$timeout = intval($this->envGet('book_lookup_timeout', 0));
if ($timeout > 0) {
$this->timeout = max(3, $timeout);
}
$budget = intval($this->envGet('book_lookup_budget', 0));
if ($budget > 0) {
$this->budget = max(5, $budget);
}
if (isset($config['timeout'])) $this->timeout = max(3, intval($config['timeout']));
if (isset($config['enabled'])) $this->enabled = (bool)$config['enabled'];
if (isset($config['google_key'])) $this->googleKey = (string)$config['google_key'];
if (isset($config['budget'])) $this->budget = max(5, intval($config['budget']));
}
private function outOfBudget()
{
return $this->deadline > 0 && microtime(true) >= $this->deadline;
}
/**
* 反查 ISBN
*
* @param array $hints title / author / year / publisher / edition / content
* @return array ['isbn'=>'','publisher'=>'','year'=>'','title'=>'','source'=>'']
*/
public function resolve(array $hints)
{
$empty = ['isbn' => '', 'publisher' => '', 'year' => '', 'title' => '', 'source' => ''];
if (!$this->enabled) {
return $empty;
}
$hints = $this->normalizeHints($hints);
if ($hints['title'] === '') {
return $empty;
}
// 原文里直接写了 ISBN 的,优先采信,不用外网
$inText = $this->extractIsbnFromText($hints['content']);
if ($inText !== '') {
return array_merge($empty, ['isbn' => $inText, 'source' => 'text']);
}
$cacheKey = 'book_' . sha1(strtolower($hints['title'] . '|' . $hints['author'] . '|' . $hints['year'] . '|' . $hints['edition']));
$cached = $this->cacheGet($cacheKey, 30 * 86400);
if (is_array($cached) && array_key_exists('isbn', $cached)) {
return array_merge($empty, $cached);
}
$this->deadline = microtime(true) + $this->budget;
$result = $this->searchOpenLibrary($hints);
if ($result['isbn'] === '' && $this->googleKey !== '' && !$this->outOfBudget()) {
$result = $this->searchGoogleBooks($hints);
}
$this->deadline = 0;
$this->cacheSet($cacheKey, $result);
return $result;
}
/**
* 从参考文献原文抽取书目线索(书名 / 作者 / 年份 / 版次 / 出版社)
*/
public function extractHints($content, array $known = [])
{
$content = trim((string)$content);
$hints = [
'title' => trim((string)($known['title'] ?? '')),
'author' => trim((string)($known['author'] ?? '')),
'year' => trim((string)($known['year'] ?? '')),
'publisher' => trim((string)($known['publisher'] ?? '')),
'edition' => '',
'content' => $content,
];
if ($content === '') {
return $hints;
}
// 章节引用In: 编者. 书名. ...)反查的是书,不是章节
$bookPart = $content;
if (preg_match('/\bIn:\s*(.+)$/i', $content, $m)) {
$bookPart = trim($m[1]);
$hints['title'] = '';
}
$hints['edition'] = $this->parseEdition($content);
if ($hints['year'] === '' && preg_match_all('/\b(?:19|20)\d{2}\b/', $content, $ym)) {
$hints['year'] = end($ym[0]);
}
// 地点: 出版社; 年份
if ($hints['publisher'] === '' && preg_match('/:\s*([^;:]{2,60}?)\s*[;,]\s*(?:19|20)\d{2}/u', $bookPart, $m)) {
$hints['publisher'] = trim($m[1], " .,");
}
if ($hints['title'] === '') {
$hints['title'] = $this->parseTitle($bookPart);
}
if ($hints['author'] === '') {
$hints['author'] = $this->parseAuthor($bookPart);
}
return $hints;
}
// ------------------------------------------------------------------
// Open Library
// ------------------------------------------------------------------
private function searchOpenLibrary(array $hints)
{
$empty = ['isbn' => '', 'publisher' => '', 'year' => '', 'title' => '', 'source' => ''];
$q = 'title:"' . $this->escapeSolr($hints['title']) . '"';
$surnames = $this->authorSurnames($hints['author']);
if (!empty($surnames)) {
$q .= ' author:' . $this->escapeSolr($surnames[0]);
}
$url = 'https://openlibrary.org/search.json?' . http_build_query([
'q' => $q,
'limit' => 5,
'fields' => 'key,title,author_name,first_publish_year,edition_count,isbn,publisher',
]);
$docs = $this->getJson($url);
$docs = isset($docs['docs']) && is_array($docs['docs']) ? $docs['docs'] : [];
if (empty($docs) && !empty($surnames) && !$this->outOfBudget()) {
// 作者写法不一致时(编者、机构作者)退回只按书名检索
$url = 'https://openlibrary.org/search.json?' . http_build_query([
'q' => 'title:"' . $this->escapeSolr($hints['title']) . '"',
'limit' => 5,
'fields' => 'key,title,author_name,first_publish_year,edition_count,isbn,publisher',
]);
$docs = $this->getJson($url);
$docs = isset($docs['docs']) && is_array($docs['docs']) ? $docs['docs'] : [];
}
// 同一本书常被拆成多个 work书名带不带副标题、换了出版社
// 只看第一个命中的 work 会漏掉真正对得上版次的那一版,所以候选放在一起比
$best = null;
$bestScore = -1;
$checked = 0;
foreach ($docs as $doc) {
if ($checked >= 3 || $this->outOfBudget()) {
break;
}
$docTitle = trim((string)($doc['title'] ?? ''));
if (!$this->titlesMatch($hints['title'], $docTitle)) {
continue;
}
// 章节引用里"作者"是章节作者、编者写法也各式各样,对不上时不直接放弃,
// 改为要求出版年精确命中且另有版次或出版社佐证才采信
$authorOk = $this->authorsMatch($surnames, $doc['author_name'] ?? []);
$key = trim((string)($doc['key'] ?? ''));
if ($key === '') {
continue;
}
$checked++;
$edition = $this->pickEdition($key, $hints);
if (!$authorOk && $edition['score'] < 70) {
continue;
}
if ($edition['isbn'] !== '' && $edition['score'] > $bestScore) {
$bestScore = $edition['score'];
$best = [
'isbn' => $edition['isbn'],
'publisher' => $edition['publisher'],
'year' => $edition['year'],
'title' => $docTitle,
'source' => 'openlibrary',
];
}
// 年份与版次都对上了,没有更好的可能,不用再翻其他 work
if ($bestScore >= 100) {
break;
}
}
return $best === null ? $empty : $best;
}
/**
* 在 work 的版本列表里挑最贴合引文的那一版。
* 年份与版次是硬条件(对不上直接淘汰),出版社和 ISBN-13 只作同分时的取舍,
* 否则会出现"出版社对上了就把 2020 年第 14 版当成 2006 年第 11 版"的错配。
*
* @return array 附带 score供跨 work 比较
*/
private function pickEdition($workKey, array $hints)
{
$empty = ['isbn' => '', 'publisher' => '', 'year' => '', 'score' => -1];
$entries = [];
for ($page = 0; $page < 2; $page++) {
$url = 'https://openlibrary.org' . $workKey . '/editions.json?limit=100&offset=' . ($page * 100);
$data = $this->getJson($url);
$batch = isset($data['entries']) && is_array($data['entries']) ? $data['entries'] : [];
if (empty($batch)) {
break;
}
$entries = array_merge($entries, $batch);
if (count($batch) < 100 || count($entries) >= intval($data['size'] ?? 0) || $this->outOfBudget()) {
break;
}
}
if (empty($entries)) {
return $empty;
}
$wantYear = intval($hints['year']);
$wantEdition = $this->editionOrdinal($hints['edition']);
$wantPublisher = $this->normalizeToken($hints['publisher']);
$hasConstraint = ($wantYear > 0 || $wantEdition > 0);
$best = null;
$bestScore = -1;
foreach ($entries as $entry) {
$isbn = $this->pickBestIsbn(array_merge(
is_array($entry['isbn_13'] ?? null) ? $entry['isbn_13'] : [],
is_array($entry['isbn_10'] ?? null) ? $entry['isbn_10'] : []
));
if ($isbn === '') {
continue;
}
$year = $this->parseYear((string)($entry['publish_date'] ?? ''));
$entryEdition = $this->editionOrdinal((string)($entry['edition_name'] ?? ''));
// 版次写明且不一致,一定不是同一版
if ($wantEdition > 0 && $entryEdition > 0 && $wantEdition !== $entryEdition) {
continue;
}
$editionMatched = ($wantEdition > 0 && $wantEdition === $entryEdition);
$score = 0;
if ($wantYear > 0) {
if ($year > 0) {
$diff = abs($wantYear - $year);
if ($diff === 0) {
$score += 60;
} elseif ($diff === 1) {
// 版权年与实际发行年常差一年
$score += 40;
} elseif ($diff <= 2 && $editionMatched) {
$score += 20;
} else {
continue;
}
} elseif (!$editionMatched) {
// 年份对不上又没有版次佐证,不敢认
continue;
}
}
if ($editionMatched) {
$score += 40;
}
if ($wantPublisher !== '') {
foreach ((array)($entry['publishers'] ?? []) as $p) {
if ($this->tokensOverlap($wantPublisher, $this->normalizeToken($p))) {
$score += 10;
break;
}
}
}
if (strlen($isbn) === 13) {
$score += 3;
}
if ($score > $bestScore) {
$bestScore = $score;
$best = [
'isbn' => $isbn,
'publisher' => $this->firstString($entry['publishers'] ?? []),
'year' => $year > 0 ? (string)$year : '',
'score' => $score,
];
}
}
if ($best === null) {
return $empty;
}
// 引文给了年份/版次,就必须真的对上其中之一
if ($hasConstraint && $bestScore < 40) {
return $empty;
}
// 引文什么线索都没给:只有单一版本的书才敢直接给 ISBN
if (!$hasConstraint && count($entries) > 3) {
return $empty;
}
return $best;
}
// ------------------------------------------------------------------
// Google Books备源
// ------------------------------------------------------------------
private function searchGoogleBooks(array $hints)
{
$empty = ['isbn' => '', 'publisher' => '', 'year' => '', 'title' => '', 'source' => ''];
$q = 'intitle:"' . $hints['title'] . '"';
$surnames = $this->authorSurnames($hints['author']);
if (!empty($surnames)) {
$q .= ' inauthor:' . $surnames[0];
}
$url = 'https://www.googleapis.com/books/v1/volumes?' . http_build_query([
'q' => $q,
'maxResults' => 5,
'key' => $this->googleKey,
]);
$data = $this->getJson($url);
$items = isset($data['items']) && is_array($data['items']) ? $data['items'] : [];
foreach ($items as $item) {
$info = isset($item['volumeInfo']) && is_array($item['volumeInfo']) ? $item['volumeInfo'] : [];
$title = trim((string)($info['title'] ?? ''));
if (!$this->titlesMatch($hints['title'], $title)) {
continue;
}
if (!$this->authorsMatch($surnames, $info['authors'] ?? [])) {
continue;
}
$year = $this->parseYear((string)($info['publishedDate'] ?? ''));
$wantYear = intval($hints['year']);
if ($wantYear > 0 && $year > 0 && abs($wantYear - $year) > 3) {
continue;
}
$candidates = [];
foreach ((array)($info['industryIdentifiers'] ?? []) as $id) {
$type = strtoupper((string)($id['type'] ?? ''));
if ($type === 'ISBN_13' || $type === 'ISBN_10') {
$candidates[] = (string)($id['identifier'] ?? '');
}
}
$isbn = $this->pickBestIsbn($candidates);
if ($isbn !== '') {
return [
'isbn' => $isbn,
'publisher' => trim((string)($info['publisher'] ?? '')),
'year' => $year > 0 ? (string)$year : '',
'title' => $title,
'source' => 'googlebooks',
];
}
}
return $empty;
}
// ------------------------------------------------------------------
// 解析与校验
// ------------------------------------------------------------------
private function normalizeHints(array $hints)
{
$out = [
'title' => trim((string)($hints['title'] ?? '')),
'author' => trim((string)($hints['author'] ?? '')),
'year' => trim((string)($hints['year'] ?? '')),
'publisher' => trim((string)($hints['publisher'] ?? '')),
'edition' => trim((string)($hints['edition'] ?? '')),
'content' => trim((string)($hints['content'] ?? '')),
];
$out['title'] = $this->cleanTitle($out['title']);
if (mb_strlen($out['title'], 'UTF-8') < 4) {
$out['title'] = '';
}
return $out;
}
private function cleanTitle($title)
{
$title = trim((string)$title);
$title = preg_replace('/\s+/u', ' ', $title);
// 去掉粘在书名后的版次、卷次、出版地
$title = preg_replace('/[\.,;]?\s*\b\d{1,2}(?:st|nd|rd|th)?\s*(?:ed\.?|edition|版)\b.*$/iu', '', $title);
$title = preg_replace('/[\.,;]?\s*\b(?:first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\s+(?:ed\.?|edition)\b.*$/iu', '', $title);
return trim($title, " .,;:");
}
private function parseTitle($text)
{
$parts = preg_split('/\.\s+/u', trim((string)$text), 3);
if (is_array($parts) && count($parts) >= 2) {
return $this->cleanTitle($parts[1]);
}
return '';
}
private function parseAuthor($text)
{
$parts = preg_split('/\.\s+/u', trim((string)$text), 2);
return is_array($parts) && count($parts) >= 2 ? trim($parts[0]) : '';
}
private function parseEdition($text)
{
if (preg_match('/\b(\d{1,2})\s*(?:st|nd|rd|th|d)?\s*(?:ed\.?|edition)\b/iu', $text, $m)) {
return $m[1];
}
if (preg_match('/\b(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\s+(?:ed\.?|edition)\b/iu', $text, $m)) {
return $m[1];
}
if (preg_match('/第\s*(\d{1,2})\s*版/u', $text, $m)) {
return $m[1];
}
return '';
}
private function editionOrdinal($edition)
{
$edition = strtolower(trim((string)$edition));
if ($edition === '') {
return 0;
}
if (preg_match('/\d{1,2}/', $edition, $m)) {
return intval($m[0]);
}
$words = [
'first' => 1, 'second' => 2, 'third' => 3, 'fourth' => 4, 'fifth' => 5,
'sixth' => 6, 'seventh' => 7, 'eighth' => 8, 'ninth' => 9, 'tenth' => 10,
];
foreach ($words as $word => $num) {
if (strpos($edition, $word) !== false) {
return $num;
}
}
return 0;
}
private function parseYear($date)
{
return preg_match('/\b(1[5-9]\d{2}|20\d{2})\b/', (string)$date, $m) ? intval($m[1]) : 0;
}
/**
* @return string[]
*/
private function authorSurnames($author)
{
$author = trim((string)$author);
if ($author === '') {
return [];
}
$surnames = [];
foreach (preg_split('/[,;]+/u', $author) as $chunk) {
$chunk = trim(preg_replace('/\b(et al\.?|eds?\.?|editors?)\b/i', '', $chunk));
if (preg_match_all('/[A-Za-z][A-Za-z\'\-]{2,}/u', $chunk, $m)) {
$surnames[] = $m[0][0];
}
}
return array_values(array_unique($surnames));
}
private function authorsMatch(array $surnames, $candidates)
{
if (empty($surnames)) {
return true;
}
$blob = strtolower(is_array($candidates) ? implode(' ', $candidates) : (string)$candidates);
if ($blob === '') {
return true;
}
foreach ($surnames as $surname) {
if (strpos($blob, strtolower($surname)) !== false) {
return true;
}
}
return false;
}
private function titlesMatch($expected, $found)
{
$a = $this->normalizeToken($expected);
$b = $this->normalizeToken($found);
if ($a === '' || $b === '') {
return false;
}
if ($a === $b || strpos($a, $b) !== false || strpos($b, $a) !== false) {
return true;
}
// 中日韩书名逐字比较不可靠,只认包含关系
if (preg_match('/[\x{4e00}-\x{9fff}]/u', $expected . $found)) {
return false;
}
similar_text($a, $b, $pct);
return $pct >= 80;
}
private function normalizeToken($text)
{
$text = strtolower(trim((string)$text));
$text = preg_replace('/[^a-z0-9\x{4e00}-\x{9fff}\s]+/u', ' ', $text);
return trim(preg_replace('/\s+/u', ' ', $text));
}
private function tokensOverlap($a, $b)
{
if ($a === '' || $b === '') {
return false;
}
if (strpos($a, $b) !== false || strpos($b, $a) !== false) {
return true;
}
$wordsA = array_filter(explode(' ', $a), function ($w) {
return strlen($w) > 3;
});
foreach ($wordsA as $word) {
if (strpos($b, $word) !== false) {
return true;
}
}
return false;
}
/**
* ISBN-13 优先,且必须通过校验位;只有 ISBN-10 时换算成等价的 ISBN-13
*/
private function pickBestIsbn($list)
{
$list = is_array($list) ? $list : [$list];
$isbn10 = '';
foreach ($list as $raw) {
$isbn = strtoupper(preg_replace('/[^0-9Xx]/', '', (string)$raw));
if (!$this->isValidIsbn($isbn)) {
continue;
}
if (strlen($isbn) === 13) {
return $isbn;
}
if ($isbn10 === '') {
$isbn10 = $isbn;
}
}
return $isbn10 === '' ? '' : $this->isbn10To13($isbn10);
}
private function isbn10To13($isbn10)
{
$body = '978' . substr($isbn10, 0, 9);
$sum = 0;
for ($i = 0; $i < 12; $i++) {
$sum += intval($body[$i]) * (($i % 2 === 0) ? 1 : 3);
}
return $body . ((10 - $sum % 10) % 10);
}
private function isValidIsbn($isbn)
{
$len = strlen($isbn);
if ($len === 13) {
if (!preg_match('/^\d{13}$/', $isbn)) {
return false;
}
$sum = 0;
for ($i = 0; $i < 12; $i++) {
$sum += intval($isbn[$i]) * (($i % 2 === 0) ? 1 : 3);
}
return ((10 - $sum % 10) % 10) === intval($isbn[12]);
}
if ($len === 10) {
if (!preg_match('/^\d{9}[\dX]$/', $isbn)) {
return false;
}
$sum = 0;
for ($i = 0; $i < 9; $i++) {
$sum += intval($isbn[$i]) * (10 - $i);
}
$sum += ($isbn[9] === 'X') ? 10 : intval($isbn[9]);
return $sum % 11 === 0;
}
return false;
}
private function extractIsbnFromText($content)
{
if (!preg_match_all('/\bISBN(?:-1[03])?[:\s]*([0-9][0-9\-\s]{8,20}[0-9Xx])/i', (string)$content, $m)) {
return '';
}
return $this->pickBestIsbn($m[1]);
}
private function firstString($list)
{
foreach ((array)$list as $item) {
$item = trim((string)$item);
if ($item !== '' && strcasecmp($item, 'Other') !== 0) {
return $item;
}
}
return '';
}
private function escapeSolr($text)
{
return trim(preg_replace('/["\\\\+\-!(){}\[\]^~*?:\/]+/u', ' ', (string)$text));
}
// ------------------------------------------------------------------
// HTTP / 缓存
// ------------------------------------------------------------------
private function getJson($url)
{
$timeout = $this->timeout;
if ($this->deadline > 0) {
$left = (int)ceil($this->deadline - microtime(true));
if ($left <= 0) {
return [];
}
$timeout = min($timeout, $left);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(5, $timeout));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'User-Agent: TMRjournals-BookLookup/1.0 (' . $this->envGet('crossref_mailto', 'support@tmrjournals.com') . ')',
]);
$res = curl_exec($ch);
$code = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
curl_close($ch);
if ($code !== 200 || !is_string($res) || $res === '') {
return [];
}
$decoded = json_decode($res, true);
return is_array($decoded) ? $decoded : [];
}
private function envGet($key, $default = '')
{
if (!class_exists('\think\Env')) {
return $default;
}
return \think\Env::get($key, $default);
}
private function cacheDir()
{
return rtrim(ROOT_PATH, '/') . '/runtime/book_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;
}
return json_decode((string)@file_get_contents($file), true);
}
private function cacheSet($key, $value)
{
$dir = $this->cacheDir();
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
@file_put_contents($dir . '/' . $key . '.json', json_encode($value, JSON_UNESCAPED_UNICODE));
}
}

View File

@@ -21,10 +21,14 @@ class ReferenceDispatchService
/** @var ReferenceMetadataService */
private $metadata;
/** @var BookMetadataService */
private $bookMetadata;
public function __construct()
{
$this->classifier = new ReferenceTypeClassifier(['use_llm' => true]);
$this->metadata = new ReferenceMetadataService();
$this->bookMetadata = new BookMetadataService();
}
/**
@@ -185,18 +189,24 @@ class ReferenceDispatchService
$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['isbn'] = !empty($raw) ? $this->extractIsbnFromRaw($raw) : '';
$update['is_ja'] = 1;
} else {
$parsed = $this->parseBookFromContent($content);
$update = array_merge($update, $parsed);
}
// 图书绝大多数没有 DOICrossref 和原文都给不出 ISBN按书目信息去外部书目库反查
if (trim((string)($update['isbn'] ?? '')) === '') {
$update = array_merge($update, $this->lookupBookIsbn($content, $update));
}
// 反查不到时退回 DOI 链接,保持 isbn 字段"可点开验证"的既有语义
if (trim((string)($update['isbn'] ?? '')) === '' && !empty($refer['refer_doi'])) {
$doi = $this->normalizeDoi($refer['refer_doi']);
$update['isbn'] = $doi !== '' ? 'https://doi.org/' . $doi : '';
}
$hasCore = trim((string)($update['author'] ?? '')) !== ''
&& trim((string)($update['title'] ?? '')) !== '';
$update['cs'] = $hasCore ? 1 : 0;
@@ -207,6 +217,43 @@ class ReferenceDispatchService
Db::name('production_article_refer')->where('p_refer_id', $pReferId)->update($update);
}
/**
* 按书名/作者/年份/版次反查 ISBN顺带补齐缺失的出版社与出版年
*
* @return array 只包含需要覆盖的字段
*/
private function lookupBookIsbn($content, array $update)
{
$known = [
'title' => (string)($update['title'] ?? ''),
'author' => (string)($update['author'] ?? ''),
'publisher' => (string)($update['joura'] ?? ''),
'year' => preg_match('/\b(19|20)\d{2}\b/', (string)($update['dateno'] ?? ''), $m) ? $m[0] : '',
];
try {
$hints = $this->bookMetadata->extractHints($content, $known);
$found = $this->bookMetadata->resolve($hints);
} catch (\Exception $e) {
\think\Log::write('book isbn lookup failed: ' . $e->getMessage(), 'error');
return [];
}
if (trim((string)$found['isbn']) === '') {
return [];
}
$patch = ['isbn' => trim((string)$found['isbn'])];
if (trim((string)($update['joura'] ?? '')) === '' && trim((string)$found['publisher']) !== '') {
$patch['joura'] = trim((string)$found['publisher']);
}
if (trim((string)($update['dateno'] ?? '')) === '' && trim((string)$found['year']) !== '') {
$patch['dateno'] = trim((string)$found['year']);
}
return $patch;
}
private function extractBookPublisher(array $raw, array $summary)
{
$publisher = trim((string)($raw['publisher'] ?? ''));