From f87057488b2c02e1805e63bf2e3058d97817a67a Mon Sep 17 00:00:00 2001
From: wangjinlei <751475802@qq.com>
Date: Fri, 7 Aug 2026 17:51:18 +0800
Subject: [PATCH] =?UTF-8?q?=E5=8F=82=E8=80=83=E6=96=87=E7=8C=AE=E7=9A=84?=
=?UTF-8?q?=E4=BD=9C=E8=80=85=E6=95=B0=E5=8F=98=E6=88=906?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
application/api/controller/Preaccept.php | 4 +-
application/api/controller/Production.php | 10 +-
application/api/controller/References.php | 22 +-
application/common.php | 13 +-
application/common/AuthorListFormatter.php | 201 ++++++++++++++++++
application/common/BookCitationParser.php | 149 +++++++++++++
application/common/CrossrefService.php | 13 +-
application/common/JournalArticle.php | 9 +-
application/common/PubmedService.php | 10 +-
.../common/ReferenceMetadataService.php | 4 +-
10 files changed, 386 insertions(+), 49 deletions(-)
create mode 100644 application/common/AuthorListFormatter.php
diff --git a/application/api/controller/Preaccept.php b/application/api/controller/Preaccept.php
index 83653445..25f31a60 100644
--- a/application/api/controller/Preaccept.php
+++ b/application/api/controller/Preaccept.php
@@ -874,8 +874,8 @@ class Preaccept extends Base
$title = trim((string)($summary['title'] ?? ''));
$jouraRaw = trim((string)($summary['joura'] ?? ''));
- // 姓全写 + 名首字母,超过 3 个作者取前 3 个 + et al
- $authorCitation = $svc->getAuthorsCitation($summary['raw'] ?? [], 3);
+ // 姓全写 + 名首字母,作者数超过 6 个才取前 3 个 + et al
+ $authorCitation = $svc->getAuthorsCitation($summary['raw'] ?? []);
$dateno = trim((string)($summary['dateno'] ?? ''));
$doilink = trim((string)($summary['doilink'] ?? ''));
if ($doilink === '') {
diff --git a/application/api/controller/Production.php b/application/api/controller/Production.php
index d6daa605..59190d66 100644
--- a/application/api/controller/Production.php
+++ b/application/api/controller/Production.php
@@ -1005,12 +1005,10 @@ class Production extends Base
public function prgeAuthor($author)
{
- $a = explode(',', $author);
- if (count($a) < 7) {
- return $author . '.';
- } else {
- return trim($a[0]) . ', ' . trim($a[1]) . ', ' . trim($a[2]) . ', et al.';
- }
+ //作者不超过 6 个全部列出,超过则只列前 3 个加 et al
+ $formatted = \app\common\AuthorListFormatter::format($author);
+
+ return $formatted === '' ? '' : rtrim($formatted, '.') . '.';
}
diff --git a/application/api/controller/References.php b/application/api/controller/References.php
index 71695d70..87ae685c 100644
--- a/application/api/controller/References.php
+++ b/application/api/controller/References.php
@@ -82,14 +82,8 @@ class References extends Base
if(!empty($aRefer['doilink'])){
$sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.');
if(!empty($sAuthor)){
- $aAuthor = explode(',', $sAuthor);
- if(count($aAuthor) > 3){
- $sAuthor = implode(',', array_slice($aAuthor, 0,3));
- $sAuthor .= ', et al';
- }
- if(count($aAuthor) <= 3 ){
- $sAuthor = implode(',', $aAuthor);
- }
+ //作者不超过 6 个全部列出,超过则只列前 3 个加 et al
+ $sAuthor = \app\common\AuthorListFormatter::format($sAuthor);
}
//文章标题
$sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.');
@@ -116,14 +110,8 @@ class References extends Base
if($aRefer['refer_type'] == 'book'){
$sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.');
if(!empty($sAuthor)){
- $aAuthor = explode(',', $sAuthor);
- if(count($aAuthor) > 3){
- $sAuthor = implode(',', array_slice($aAuthor, 0,3));
- $sAuthor .= ', et al';
- }
- if(count($aAuthor) <= 3 ){
- $sAuthor = implode(',', $aAuthor);
- }
+ //作者不超过 6 个全部列出,超过则只列前 3 个加 et al
+ $sAuthor = \app\common\AuthorListFormatter::format($sAuthor);
}
//文章标题
$sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.');
@@ -1214,7 +1202,7 @@ class References extends Base
$sSysMessagePrompt = '请完成以下任务:
1. 根据提供的DOI号,查询该文献的AMA引用格式;
2. 按照以下规则调整AMA引用格式:
- - 第三个作者名字后添加 et al.;
+ - 作者不超过6个时全部列出;超过6个时只保留前3个作者,并在第三个作者名字后添加 et al.;
- DOI前加上"Available at: ";
- DOI信息格式调整为"https://doi.org/+真实DOI"(替换真实DOI为文献实际DOI).
3. 严格按照以下JSON结构返回结果,仅返回JSON数据,不要额外文字,包含字段:doilink(url格式)、title(标题)、author(作者数组)、joura(出版社名称)、dateno(年;卷(期):起始页-终止页),is_ai_check(默认1)
diff --git a/application/common.php b/application/common.php
index 5d13133d..834df4df 100644
--- a/application/common.php
+++ b/application/common.php
@@ -651,7 +651,8 @@ function formateAuthor($list){
if(isset($list['given_name'])||isset($list['surname'])){
$flag = $list['given_name']." ".$list['surname'];
}
- else if (count($list)<=3){
+ //作者不超过 6 个全部列出,超过则只列前 3 个加 et al
+ else if (count($list)<=6){
foreach ($list as $v){
$flag .= $v['given_name']." ".$v['surname'].", ";
}
@@ -904,12 +905,10 @@ function formateJournal($fullname)
function prgeAuthor($author)
{
- $a = explode(',', $author);
- if (count($a) < 7) {
- return $author . '.';
- } else {
- return trim($a[0]) . ', ' . trim($a[1]) . ', ' . trim($a[2]) . ', et al.';
- }
+ //作者不超过 6 个全部列出,超过则只列前 3 个加 et al
+ $formatted = \app\common\AuthorListFormatter::format($author);
+
+ return $formatted === '' ? '' : rtrim($formatted, '.') . '.';
}
diff --git a/application/common/AuthorListFormatter.php b/application/common/AuthorListFormatter.php
new file mode 100644
index 00000000..d94649d5
--- /dev/null
+++ b/application/common/AuthorListFormatter.php
@@ -0,0 +1,201 @@
+ $max) {
+ return implode(', ', array_slice($units, 0, $keep)) . ', et al';
+ }
+
+ return implode(', ', $units);
+ }
+
+ public static function countAuthors($author)
+ {
+ return count(self::split($author));
+ }
+
+ public static function isTruncated($author)
+ {
+ return (bool)preg_match('/\bet\s+al\.?\s*$/iu', (string)$author);
+ }
+
+ /**
+ * 拆成单个作者
+ *
+ * @return string[]
+ */
+ public static function split($author)
+ {
+ $author = self::normalize($author);
+ $author = preg_replace('/[,;]?\s*\bet\s+al\.?\s*$/iu', '', $author);
+ $author = trim($author, " ,;.");
+ if ($author === '') {
+ return [];
+ }
+
+ if (strpos($author, ';') !== false) {
+ // 分号是无歧义的作者分隔符
+ $units = preg_split('/\s*;\s*/u', $author);
+ } else {
+ $text = $author;
+ // "A, B, and C" 里的 and/& 才是分隔符;没有逗号时它多半是机构名的一部分
+ // (如 "National Institute for Health and Care Excellence"),不能切
+ if (strpos($text, ',') !== false) {
+ $text = preg_replace('/\s*,?\s+(?:and|&)\s+/iu', ', ', $text);
+ }
+ $units = preg_split('/\s*,\s*/u', $text);
+ }
+
+ $units = array_values(array_filter(array_map(function ($u) {
+ return trim($u, " ,;");
+ }, (array)$units), 'strlen'));
+
+ $units = self::mergeGivenNames($units);
+
+ // 整串没有一个单元像人名,多半是带逗号的机构名,算一位作者
+ $hasPerson = false;
+ foreach ($units as $unit) {
+ if (self::looksLikePerson($unit)) {
+ $hasPerson = true;
+ break;
+ }
+ }
+
+ return $hasPerson ? $units : [$author];
+ }
+
+ /**
+ * 合并 APA 的 "姓, 名" —— "Smith", "A. B." → "Smith, A. B."
+ *
+ * @param string[] $units
+ * @return string[]
+ */
+ private static function mergeGivenNames(array $units)
+ {
+ $merged = [];
+ foreach ($units as $unit) {
+ // 上一位作者已经带了名缩写,说明这一段是新作者的姓,不能再往上并
+ $last = empty($merged) ? '' : $merged[count($merged) - 1];
+ if ($last !== '' && !self::hasInitials($last) && self::looksLikeGivenName($unit)) {
+ $merged[count($merged) - 1] .= ', ' . $unit;
+ continue;
+ }
+ $merged[] = $unit;
+ }
+
+ return $merged;
+ }
+
+ private static function hasInitials($unit)
+ {
+ return (bool)preg_match('/\p{Lu}\./u', $unit) || (bool)preg_match('/\s\p{Lu}{1,4}$/u', $unit);
+ }
+
+ /**
+ * 是否只是名/缩写(属于上一个姓):每个词都是首字母大写,且至少有一个是"单字母 + 点"
+ * "A. B." / "H. Karl." → 是;"Jones AC" / "Bruce Alberts" → 否
+ */
+ private static function looksLikeGivenName($unit)
+ {
+ $unit = trim((string)$unit);
+ if ($unit === '') {
+ return false;
+ }
+ // 末位作者的名常被上游去掉尾点,"K." 会变成 "K"
+ if (preg_match('/^\p{Lu}\.?$/u', $unit)) {
+ return true;
+ }
+ if (!preg_match('/\b\p{Lu}\./u', $unit)) {
+ return false;
+ }
+
+ foreach (preg_split('/\s+/u', $unit) as $word) {
+ $word = trim($word, " .");
+ if ($word === '') {
+ continue;
+ }
+ if (!preg_match('/^\p{Lu}[\p{L}]*$/u', $word)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 判定要偏严:机构名里的逗号片段("Department of Health" / "Education")不能算人名,
+ * 否则 "Department of Health, Education, and Welfare" 会被当成三位作者
+ */
+ private static function looksLikePerson($unit)
+ {
+ $unit = trim(preg_replace('/\s+/u', ' ', str_replace(',', ' ', (string)$unit)));
+ if ($unit === '') {
+ return false;
+ }
+ // 中文姓名
+ if (preg_match('/^[\x{4e00}-\x{9fff}·]{2,10}$/u', $unit)) {
+ return true;
+ }
+ // 以缩写结尾:"Smith AB" / "van den Berg AB"
+ if (preg_match('/^\p{Lu}.*\s\p{Lu}{1,4}$/u', $unit)) {
+ return true;
+ }
+ // 带点的名缩写:"Smith A. B." / "Butcher H. Karl."
+ if (preg_match('/\b\p{Lu}\./u', $unit)) {
+ return true;
+ }
+
+ // 两个词的全名:"Bruce Alberts"
+ return (bool)preg_match('/^\p{Lu}[\p{L}\'\x{2019}\-]+\s+\p{Lu}[\p{L}\'\x{2019}\-]+$/u', $unit);
+ }
+
+ private static function normalize($author)
+ {
+ $author = trim((string)$author);
+ $author = str_replace([',', ';', '.', ' '], [',', ';', '.', ' '], $author);
+ $author = preg_replace('/\s+/u', ' ', $author);
+
+ return trim($author, " ,;");
+ }
+}
diff --git a/application/common/BookCitationParser.php b/application/common/BookCitationParser.php
index 2b96482b..9ea59530 100644
--- a/application/common/BookCitationParser.php
+++ b/application/common/BookCitationParser.php
@@ -47,6 +47,15 @@ class BookCitationParser
$out['isbn'] = $this->matchIsbn($clean);
$clean = $this->stripTail($clean);
+ // APA 格式:"作者 (年份). 书名 (第N版). 出版社." —— 年份括号是很硬的锚点,优先按它切
+ if ($this->takeApa($clean, $out)) {
+ if ($this->needsLlm($out)) {
+ $out = $this->refineByLlm($clean, $out);
+ }
+
+ return $out;
+ }
+
// 章节引用:"章节作者. 章节名. In: 编者. 书名. 版次. 地点: 出版社; 年. 页码"
// 出版信息属于 In: 之后的那本书,必须分开解析,否则会把编者当成书名
if (preg_match('/^(.*?)\bIn\s*:\s*(.+)$/isu', $clean, $m) && trim($m[1]) !== '' && trim($m[2]) !== '') {
@@ -126,6 +135,146 @@ class BookCitationParser
return preg_replace('/[^0-9Xx]/', '', $m[1]);
}
+ // ------------------------------------------------------------------
+ // APA 格式
+ // ------------------------------------------------------------------
+
+ /**
+ * "Duffy, E., Hockenberry, M., & Gibbs, K. (2023). Wong's Nursing Care of
+ * Infants and Children (12th ed.). Elsevier."
+ */
+ private function takeApa($text, array &$out)
+ {
+ if (!preg_match('/^(.{2,300}?)\s*\(\s*(\d{4})[a-z]?\s*\)\s*\.\s*(.+)$/su', $text, $m)) {
+ return false;
+ }
+
+ $author = preg_replace('/\(\s*(?:Ed|Eds|Editor|Editors)\.?\s*\)/iu', '', $m[1]);
+ $out['author'] = $this->normalizeApaAuthors(trim($author, " .,&"));
+ $out['year'] = $m[2];
+
+ // 章节引用:"章节名. In A. Editor (Ed.), 书名 (pp. 1-10). 出版社."
+ // 必须先认出 (Ed.) 再摘括注,否则 (Ed.) 会被当成版次括注先摘掉
+ $rest = trim($m[3]);
+ $containerPart = '';
+ if (preg_match('/^(.+?)[\.,]\s*\bIn\b\s+.+?\(\s*Eds?\.?\s*\)\s*,\s*(.+)$/isu', $rest, $cm)) {
+ $rest = trim($cm[1]);
+ $containerPart = trim($cm[2]);
+ }
+
+ $rest = $this->takeApaParentheticals($rest, $out);
+ if ($containerPart !== '') {
+ $containerPart = $this->takeApaParentheticals($containerPart, $out);
+ }
+
+ $segments = $this->apaSegments($containerPart !== '' ? $containerPart : $rest);
+ if (empty($segments)) {
+ return false;
+ }
+
+ // APA 的出版社在最后一段,其余都算书名
+ if (count($segments) >= 2) {
+ $out['publisher'] = array_pop($segments);
+ }
+ $mainTitle = $this->cleanTitle(implode('. ', $segments));
+
+ if ($containerPart !== '') {
+ $out['container'] = $mainTitle;
+ $chapter = $this->apaSegments($rest);
+ $out['title'] = $this->cleanTitle(implode('. ', $chapter));
+ } else {
+ $out['title'] = $mainTitle;
+ }
+
+ return $out['title'] !== '';
+ }
+
+ /**
+ * 摘掉 APA 的括注版次与页码:"(12th ed.)"、"(pp. 1-10)"、"(2nd ed., pp. 1-10)",
+ * 书名自带的括注(如 "(NIC)")不含 ed./pp.,会原样留下
+ */
+ private function takeApaParentheticals($text, array &$out)
+ {
+ if (!preg_match_all('/\(([^)]*(?:\bedn?\.|\bedition\b|\bpp?\.)[^)]*)\)/iu', $text, $ms, PREG_SET_ORDER)) {
+ return $text;
+ }
+
+ foreach ($ms as $item) {
+ $inner = $item[1];
+ $edition = $this->matchEdition($inner);
+ if ($edition !== '' && $out['edition'] === '') {
+ $out['edition'] = $edition;
+ }
+ if ($out['pages'] === ''
+ && preg_match('/\bpp?\.\s*([\dA-Za-z]+(?:\s*[-\x{2013}]\s*[\dA-Za-z]+)?)/iu', $inner, $pm)) {
+ $out['pages'] = preg_replace('/\s+/', '', $pm[1]);
+ }
+ $text = str_replace($item[0], ' ', $text);
+ }
+
+ return trim(preg_replace('/\s+/u', ' ', $text));
+ }
+
+ /**
+ * @return string[]
+ */
+ private function apaSegments($text)
+ {
+ $segments = [];
+ foreach ($this->splitSegments($text) as $seg) {
+ $seg = trim($seg, " .,;");
+ if ($seg !== '') {
+ $segments[] = $seg;
+ }
+ }
+
+ return $segments;
+ }
+
+ /**
+ * APA 姓名表 → 温哥华式,与期刊那条链路和 References 的渲染方式保持一致
+ * "Duffy, E., Hockenberry, M., & Gibbs, K." → "Duffy E, Hockenberry M, Gibbs K"
+ */
+ private function normalizeApaAuthors($author)
+ {
+ $author = trim((string)$author);
+ $author = preg_replace('/\s*&\s*/u', ', ', $author);
+ $author = preg_replace('/(\s*,\s*)+/u', ', ', $author);
+ if ($author === '') {
+ return '';
+ }
+
+ // 没有"姓, 名缩写"结构的(机构作者等)原样保留
+ if (!preg_match('/,\s*\p{Lu}[\p{L}]*\./u', $author)) {
+ return $author;
+ }
+
+ $tokens = preg_split('/\s*,\s*/u', $author);
+ if (!is_array($tokens) || count($tokens) < 2 || count($tokens) % 2 !== 0) {
+ return $author;
+ }
+
+ $names = [];
+ for ($i = 0; $i < count($tokens); $i += 2) {
+ $surname = trim($tokens[$i], " .");
+ $given = trim($tokens[$i + 1], " .");
+ if (!preg_match('/^\p{Lu}[\p{L}\'\-\s]*$/u', $surname)
+ || !preg_match('/^\p{Lu}[\p{L}\.\s]*$/u', $given)) {
+ return $author;
+ }
+
+ $initials = '';
+ foreach (preg_split('/[\s\.]+/u', $given) as $word) {
+ if ($word !== '') {
+ $initials .= mb_strtoupper(mb_substr($word, 0, 1), 'UTF-8');
+ }
+ }
+ $names[] = $initials === '' ? $surname : $surname . ' ' . $initials;
+ }
+
+ return implode(', ', $names);
+ }
+
// ------------------------------------------------------------------
// 作者
// ------------------------------------------------------------------
diff --git a/application/common/CrossrefService.php b/application/common/CrossrefService.php
index b2ebc00e..ba8bb8bf 100644
--- a/application/common/CrossrefService.php
+++ b/application/common/CrossrefService.php
@@ -399,14 +399,16 @@ class CrossrefService
}
/**
- * 引用格式作者串:姓全写 + 名首字母,超过 $maxAuthors 个取前 N 个 + et al
- * 例:Smith JA, Jones B, Lee C, et al
+ * 引用格式作者串:姓全写 + 名首字母
+ * 著录规则:作者数 <= $maxAuthors 时全部列出,超过时只列前 $keep 个再加 et al
+ * 例:7 个作者 → Smith JA, Jones B, Lee C, et al
*
* @param array $aDoiInfo Crossref message
- * @param int $maxAuthors 最多展示作者数,超过则截断加 et al
+ * @param int $maxAuthors 全部列出的上限,超过则截断
+ * @param int $keep 截断后保留的作者数
* @return string
*/
- public function getAuthorsCitation($aDoiInfo = [], $maxAuthors = 3)
+ public function getAuthorsCitation($aDoiInfo = [], $maxAuthors = 6, $keep = 3)
{
$list = [];
if (!empty($aDoiInfo['author'])) {
@@ -436,8 +438,9 @@ class CrossrefService
}
$maxAuthors = max(1, (int)$maxAuthors);
+ $keep = min(max(1, (int)$keep), $maxAuthors);
if (count($list) > $maxAuthors) {
- $list = array_slice($list, 0, $maxAuthors);
+ $list = array_slice($list, 0, $keep);
return implode(', ', $list) . ', et al';
}
diff --git a/application/common/JournalArticle.php b/application/common/JournalArticle.php
index febd9e64..d850195e 100644
--- a/application/common/JournalArticle.php
+++ b/application/common/JournalArticle.php
@@ -163,12 +163,9 @@ class JournalArticle
}
$sDoi = empty($v['doi']) ? '' : self::$sDoiUrl.$v['doi'];
//作者
- $aAuthorInfo = empty($v['abbr']) ? [] : explode(', ', str_replace([', ',','], ', ', $v['abbr']));
- if(count($aAuthorInfo) > 3){
- $sAuthorInfo = implode(', ', array_slice($aAuthorInfo,0,3)).", et al.";
- }else{
- $sAuthorInfo = empty($aAuthorInfo) ? '' : implode(', ', $aAuthorInfo).'.';
- }
+ //作者不超过 6 个全部列出,超过则只列前 3 个加 et al
+ $sAuthorInfo = empty($v['abbr']) ? '' : \app\common\AuthorListFormatter::format($v['abbr']);
+ $sAuthorInfo = $sAuthorInfo === '' ? '' : $sAuthorInfo.'.';
$sArticleInfo .= $i.'. Article Title: '.$v['title'].'
Author(s): '.$sAuthorInfo.'
Link or DOI: '.$sDoi.'
';
$i++;
}
diff --git a/application/common/PubmedService.php b/application/common/PubmedService.php
index d68b0832..75812173 100644
--- a/application/common/PubmedService.php
+++ b/application/common/PubmedService.php
@@ -406,10 +406,11 @@ class PubmedService
}
/**
- * 引用格式作者串:姓全写 + 名首字母,超过 $maxAuthors 个取前 N 个 + et al
- * 例:Smith JA, Jones B, Lee C, et al
+ * 引用格式作者串:姓全写 + 名首字母
+ * 著录规则:作者数 <= $maxAuthors 时全部列出,超过时只列前 $keep 个再加 et al
+ * 例:7 个作者 → Smith JA, Jones B, Lee C, et al
*/
- public function authorsCitation(array $authors, int $maxAuthors = 3): string
+ public function authorsCitation(array $authors, int $maxAuthors = 6, int $keep = 3): string
{
$list = [];
foreach ($authors as $a) {
@@ -435,8 +436,9 @@ class PubmedService
}
$maxAuthors = max(1, $maxAuthors);
+ $keep = min(max(1, $keep), $maxAuthors);
if (count($list) > $maxAuthors) {
- return implode(', ', array_slice($list, 0, $maxAuthors)) . ', et al';
+ return implode(', ', array_slice($list, 0, $keep)) . ', et al';
}
return implode(', ', $list);
}
diff --git a/application/common/ReferenceMetadataService.php b/application/common/ReferenceMetadataService.php
index e86bb91d..24e2d9bc 100644
--- a/application/common/ReferenceMetadataService.php
+++ b/application/common/ReferenceMetadataService.php
@@ -140,12 +140,12 @@ class ReferenceMetadataService
{
$authors = isset($pub['authors']) && is_array($pub['authors']) ? $pub['authors'] : [];
if (!empty($authors)) {
- $citation = $this->pubmed->authorsCitation($authors, 3);
+ $citation = $this->pubmed->authorsCitation($authors);
if ($citation !== '') {
return $citation;
}
}
- return $this->crossref->getAuthorsCitation($cr['raw'] ?? [], 3);
+ return $this->crossref->getAuthorsCitation($cr['raw'] ?? []);
}
/**