diff --git a/application/api/controller/Author.php b/application/api/controller/Author.php index 99b22daa..f024a1f2 100644 --- a/application/api/controller/Author.php +++ b/application/api/controller/Author.php @@ -4,6 +4,7 @@ namespace app\api\controller; use app\common\service\AuthorBackgroundService; use think\Controller; +use think\Db; /** * 作者背调:HTML 报告页 + JSON API @@ -15,6 +16,9 @@ class Author extends Controller /** @var AuthorBackgroundService */ private $bgService; + /** @var string */ + private $articleLookupError = ''; + public function __construct(\think\Request $request = null) { parent::__construct($request); @@ -25,7 +29,8 @@ class Author extends Controller * 作者背调 HTML 页面入口 * * 1. 传了 ORCID → 直接生成报告 - * 2. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID;1 条直接报告,多条显示选择列表 + * 2. 传了 articleId(稿件作者 ID,即 art_aut_id)→ 从 t_article_author 补全 ORCID / 姓名 / 机构 + * 3. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID;1 条直接报告,多条显示选择列表 */ public function index() { @@ -33,6 +38,18 @@ class Author extends Controller $formAction = $this->resolveFormAction(); $params = $this->resolveBackgroundParams(); + + if ($this->articleLookupError !== '') { + $this->assign([ + 'form_action' => $formAction, + 'error_msg' => $this->articleLookupError, + 'last_name' => $params['last_name'], + 'first_name' => $params['first_name'], + 'institution' => $params['institution'], + ]); + return $this->fetch('author/index'); + } + $orcidNorm = $this->bgService->normalizeOrcid($params['orcid']); if ($orcidNorm === '' @@ -129,6 +146,8 @@ class Author extends Controller */ private function resolveBackgroundParams() { + $this->articleLookupError = ''; + $pick = function (...$keys) { foreach ($keys as $k) { $v = trim((string) input('param.' . $k, '')); @@ -145,14 +164,142 @@ class Author extends Controller return ''; }; + $orcid = $pick('orcid', 'orcid_id'); + $lastName = $pick('lastName', 'last_name', 'lastname', 'surname'); + $firstName = $pick('firstName', 'first_name', 'firstname', 'given_name'); + $institution = $pick('institution', 'affiliation', 'affil', 'org'); + $realname = $pick('realname', 'real_name'); + $artAutId = $pick( 'art_aut_id', 'artAutId'); + + if ($artAutId !== '') { + $fromAuthor = $this->loadAuthorByArtAutId($artAutId); + if ($fromAuthor === null) { + $this->articleLookupError = '未找到该作者信息'; + } else { + if ($orcid === '') { + $orcid = $fromAuthor['orcid']; + } + if ($lastName === '') { + $lastName = $fromAuthor['last_name']; + } + if ($firstName === '') { + $firstName = $fromAuthor['first_name']; + } + if ($institution === '') { + $institution = $fromAuthor['institution']; + } + } + } + + if ($realname !== '' && ($lastName === '' || $firstName === '')) { + $parsed = $this->parseRealname($realname); + if ($lastName === '') { + $lastName = $parsed['last_name']; + } + if ($firstName === '') { + $firstName = $parsed['first_name']; + } + } + return [ - 'orcid' => $pick('orcid', 'orcid_id'), - 'last_name' => $pick('lastName', 'last_name', 'lastname', 'surname'), - 'first_name' => $pick('firstName', 'first_name', 'firstname', 'given_name'), - 'institution' => $pick('institution', 'affiliation', 'affil', 'org'), + 'orcid' => $orcid, + 'last_name' => $lastName, + 'first_name' => $firstName, + 'institution' => $institution, ]; } + /** + * 按 art_aut_id 从 t_article_author 读取作者信息 + */ + private function loadAuthorByArtAutId($artAutId) + { + $artAutId = (int) $artAutId; + if ($artAutId <= 0) { + return null; + } + + $row = Db::name('article_author') + ->field('orcid,firstname,lastname,company') + ->where(['art_aut_id' => $artAutId, 'state' => 0]) + ->find(); + + if (empty($row)) { + return null; + } + + return [ + 'orcid' => trim((string) ($row['orcid'] ?? '')), + 'first_name' => trim((string) ($row['firstname'] ?? '')), + 'last_name' => trim((string) ($row['lastname'] ?? '')), + 'institution' => $this->extractInstitutionFromCompany($row['company'] ?? ''), + ]; + } + + /** + * 从 company 字段提取机构名(去掉序号前缀,支持 ; 和 , 分隔) + */ + private function extractInstitutionFromCompany($company) + { + $company = trim((string) $company); + if ($company === '') { + return ''; + } + + $company = str_replace(',', ',', $company); + $parts = preg_split('/[;,]/u', $company); + $institutions = []; + + foreach ($parts as $part) { + $part = trim($part); + if ($part === '') { + continue; + } + $part = preg_replace('/^\d+\s*/', '', $part); + $part = trim($part); + if ($part !== '' && !in_array($part, $institutions, true)) { + $institutions[] = $part; + } + } + + return implode(';', $institutions); + } + + /** + * 将整段姓名拆成名+姓(如 Chuanying ZHANG → first=Chuanying, last=ZHANG) + */ + private function parseRealname($realname) + { + $realname = trim((string) $realname); + if ($realname === '') { + return ['first_name' => '', 'last_name' => '']; + } + + if (strpos($realname, ',') !== false) { + $parts = array_map('trim', explode(',', $realname, 2)); + $family = $parts[0] ?? ''; + $given = $parts[1] ?? ''; + if ($family !== '' && $given !== '') { + return ['first_name' => $given, 'last_name' => $family]; + } + } + + $tokens = preg_split('/\s+/u', $realname); + $tokens = array_values(array_filter($tokens, function ($t) { + return $t !== ''; + })); + if (count($tokens) === 0) { + return ['first_name' => '', 'last_name' => '']; + } + if (count($tokens) === 1) { + return ['first_name' => '', 'last_name' => $tokens[0]]; + } + + $lastName = array_pop($tokens); + $firstName = implode(' ', $tokens); + return ['first_name' => $firstName, 'last_name' => $lastName]; + } + private function resolveFormAction() { return rtrim($this->request->root(), '/') . '/api/author/index'; diff --git a/application/api/controller/EmailClient.php b/application/api/controller/EmailClient.php index 63841fc0..8fda8ff3 100644 --- a/application/api/controller/EmailClient.php +++ b/application/api/controller/EmailClient.php @@ -2130,6 +2130,7 @@ class EmailClient extends Base $state = $this->request->param('state', '-1'); $page = max(1, intval($this->request->param('page', 1))); $perPage = max(1, min(intval($this->request->param('per_page', 50)), 200)); + $keyword = trim($this->request->param('keyword', '')); if (!$taskId) { return jsonError('task_id is required'); @@ -2139,7 +2140,9 @@ class EmailClient extends Base if ($state !== '-1' && $state !== '') { $where['l.state'] = intval($state); } - + if ($keyword !== '') { + $where['e.email|e.name'] = ['like',"%".trim($keyword)."%"]; + } $total = Db::name('promotion_email_log')->alias('l')->where($where)->count(); $list = Db::name('promotion_email_log')->alias('l') ->join('t_expert e', 'l.expert_id = e.expert_id', 'LEFT') diff --git a/application/api/view/author/_styles.html b/application/api/view/author/_styles.html index d84863ec..19d52582 100644 --- a/application/api/view/author/_styles.html +++ b/application/api/view/author/_styles.html @@ -289,6 +289,57 @@ a.ext:hover { text-decoration: underline; } margin: 6px 0; } +.report-rules { + margin-top: 32px; + padding: 18px 22px; + background: #f8fafc; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 13px; + color: var(--text-muted); + line-height: 1.7; +} +.report-rules-title { + font-size: 14px; + font-weight: 700; + color: var(--text); + margin: 0 0 12px; +} +.report-rules-sub { + font-size: 13px; + font-weight: 600; + color: var(--text); + margin: 14px 0 8px; +} +.rules-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.rules-table th, +.rules-table td { + border: 1px solid var(--border); + padding: 8px 10px; + text-align: left; + vertical-align: top; +} +.rules-table th { + background: #edf2f7; + color: var(--text); + font-weight: 600; + width: 22%; +} +.rules-table td strong { + color: var(--text); +} +.report-rules ul { + margin: 0; + padding-left: 18px; +} +.report-rules li { + margin: 4px 0; +} + .report-foot { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-muted); text-align: center; line-height: 1.7; diff --git a/application/api/view/author/index.html b/application/api/view/author/index.html index 380184e7..bd60a5ef 100644 --- a/application/api/view/author/index.html +++ b/application/api/view/author/index.html @@ -26,19 +26,19 @@
- +
- +
- +
- +
diff --git a/application/api/view/author/report.html b/application/api/view/author/report.html index b67033f3..c9205318 100644 --- a/application/api/view/author/report.html +++ b/application/api/view/author/report.html @@ -239,6 +239,32 @@ {/notempty} +
+

学术指标说明

+ + + + + + + +
论文总数OpenAlex、ORCID、PubMed、Scopus 四项中的最大值(非相加)。OpenAlex 无档案时,可由 ORCID / PubMed 等补全。
总被引仅来自 OpenAlex;未匹配到作者档案时为 0。
H 指数仅来自 OpenAlex;未匹配时为 0。
i10 指数仅来自 OpenAlex(至少 10 次被引的论文数);未匹配时为 0。
PubMed独立统计,与论文总数口径不同。检索顺序:ORCID → 姓名+机构 → 姓名,采用第一个有结果的检索式;列表最多显示最近 10 篇。
研究方向来自 OpenAlex 前 5 个主题;无 OpenAlex 档案时不显示。
+

风险评级

+ +

其他说明

+ +
+

数据来源:OpenAlex · ORCID · PubMed · Scopus · Retraction Watch
适用于青年编委 / 特约审稿人 / 作者资质初审 diff --git a/application/common/service/AuthorBackgroundService.php b/application/common/service/AuthorBackgroundService.php index 469dbcda..0541d287 100644 --- a/application/common/service/AuthorBackgroundService.php +++ b/application/common/service/AuthorBackgroundService.php @@ -989,9 +989,14 @@ class AuthorBackgroundService } $papers = $this->pubmedFetchSummaries($ids); } + $urlTerm = $usedTerm; + if (preg_match('/^(.+)\[ORCID\]$/i', $usedTerm, $m)) { + $urlTerm = $m[1]; + } + return [ 'total' => $total, 'papers' => $papers, 'query' => $usedTerm, - 'pubmed_url' => 'https://pubmed.ncbi.nlm.nih.gov/?term=' . urlencode($usedTerm), + 'pubmed_url' => 'https://pubmed.ncbi.nlm.nih.gov/?term=' . urlencode($urlTerm), ]; }