背调优化

发邮件记录添加姓名和邮箱模糊搜索
This commit is contained in:
wyn
2026-06-05 13:52:35 +08:00
parent 752494dbdb
commit 1d54946fef
6 changed files with 243 additions and 11 deletions

View File

@@ -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 + 姓氏(机构选填)→ 仅按姓名搜 ORCID1 条直接报告,多条显示选择列表
* 2. 传了 articleId稿件作者 ID即 art_aut_id→ 从 t_article_author 补全 ORCID / 姓名 / 机构
* 3. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID1 条直接报告,多条显示选择列表
*/
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';

View File

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

View File

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

View File

@@ -26,19 +26,19 @@
<form method="get" action="{$form_action}">
<div class="form-group">
<label class="form-label">ORCID <span class="opt">— 填写后直接出报告</span></label>
<input class="form-input" type="text" name="orcid" placeholder="0000-0002-6388-7847" value="{$orcid|default=''|htmlspecialchars}">
<input class="form-input" type="text" name="orcid" value="{$orcid|default=''|htmlspecialchars}">
</div>
<div class="form-group">
<label class="form-label">姓 Last Name <span class="opt">— 未填 ORCID 时必填</span></label>
<input class="form-input" type="text" name="lastName" placeholder="PENG" value="{$last_name|default=''|htmlspecialchars}">
<input class="form-input" type="text" name="lastName" value="{$last_name|default=''|htmlspecialchars}">
</div>
<div class="form-group">
<label class="form-label">名 First Name <span class="opt">— 选填</span></label>
<input class="form-input" type="text" name="firstName" placeholder="Sijing" value="{$first_name|default=''|htmlspecialchars}">
<input class="form-input" type="text" name="firstName" value="{$first_name|default=''|htmlspecialchars}">
</div>
<div class="form-group">
<label class="form-label">机构 Institution <span class="opt">— 选填,用于候选列表排序</span></label>
<input class="form-input" type="text" name="institution" placeholder="University of Ibadan" value="{$institution|default=''|htmlspecialchars}">
<input class="form-input" type="text" name="institution" value="{$institution|default=''|htmlspecialchars}">
</div>
<button class="btn-primary" type="submit">生成背调报告</button>
</form>

View File

@@ -239,6 +239,32 @@
</div>
{/notempty}
<div class="report-rules">
<p class="report-rules-title">学术指标说明</p>
<table class="rules-table">
<tr><th>论文总数</th><td><strong>OpenAlex、ORCID、PubMed、Scopus</strong> 四项中的<strong>最大值</strong>非相加。OpenAlex 无档案时,可由 ORCID / PubMed 等补全。</td></tr>
<tr><th>总被引</th><td>仅来自 <strong>OpenAlex</strong>;未匹配到作者档案时为 0。</td></tr>
<tr><th>H 指数</th><td>仅来自 <strong>OpenAlex</strong>;未匹配时为 0。</td></tr>
<tr><th>i10 指数</th><td>仅来自 <strong>OpenAlex</strong>(至少 10 次被引的论文数);未匹配时为 0。</td></tr>
<tr><th>PubMed</th><td>独立统计,与论文总数<strong>口径不同</strong>。检索顺序ORCID → 姓名+机构 → 姓名,采用第一个有结果的检索式;列表最多显示最近 10 篇。</td></tr>
<tr><th>研究方向</th><td>来自 OpenAlex 前 5 个主题;无 OpenAlex 档案时不显示。</td></tr>
</table>
<p class="report-rules-sub">风险评级</p>
<ul>
<li><strong>高风险</strong>Retraction Watch 存在学术不端相关记录</li>
<li><strong>中风险</strong>:存在撤稿 / 关注声明</li>
<li><strong>待核实</strong>:论文总数为 0</li>
<li><strong>低风险</strong>H ≥ 10 或论文总数 ≥ 20且无上述不良记录</li>
<li><strong>一般</strong>:其余情况(青年学者常见产出区间)</li>
</ul>
<p class="report-rules-sub">其他说明</p>
<ul>
<li>OpenAlex 匹配:优先 ORCID 直连;有 ORCID 时不使用同名他人数据。</li>
<li>诚信记录:有 DOI 的 ORCID 作品按 DOI 精确比对;无 DOI 时回退姓名+题目匹配(同名需人工核实)。</li>
<li>论文总数有值、总被引/H 为 0通常表示 OpenAlex 未收录该作者,不代表无学术产出。</li>
</ul>
</div>
<p class="report-foot">
数据来源OpenAlex · ORCID · PubMed · Scopus · Retraction Watch<br>
适用于青年编委 / 特约审稿人 / 作者资质初审

View File

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