Merge remote-tracking branch 'origin/master'

This commit is contained in:
wangjinlei
2026-06-05 15:50:53 +08:00
10 changed files with 2371 additions and 262 deletions

View File

@@ -1,303 +1,302 @@
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2026/6/2
* Time: 15:04
*/
namespace app\api\controller;
use app\common\service\AuthorBackgroundService;
use think\Controller;
class Author
/**
* 作者背调HTML 报告页 + JSON API
*
* 主接口index / background_report / due_diligence
*/
class Author extends Controller
{
public function get_hindex()
/** @var AuthorBackgroundService */
private $bgService;
public function __construct(\think\Request $request = null)
{
$name = trim(input('get.name'));
$affil = trim(input('get.affil'));
$debug = (int) input('get.debug', 0);
$cookieFile = tempnam(sys_get_temp_dir(), 'scopus_cookie_');
if (empty($name)) {
return json(['code' => 0, 'msg' => '请输入作者姓名']);
}
// 1) 获取 freelookup 页面,用于拿到真实提交地址和隐藏字段。
$lookupUrl = 'https://www.scopus.com/freelookup/form/author.uri?zone=TopNavBar&origin=NO%20ORIGIN%20DEFINED';
$lookupRes = $this->httpRequest($lookupUrl, null, true, '', $cookieFile);
if (!$lookupRes['ok']) {
@unlink($cookieFile);
$ret = ['code' => 0, 'msg' => '访问 Scopus 失败:' . $lookupRes['msg']];
if ($debug === 1) {
$ret['debug'] = $this->buildDebugInfo($lookupRes['url'], $lookupRes['http_code'], $lookupRes['body']);
}
return json($ret);
}
$formInfo = $this->extractScopusLookupForm($lookupRes['body']);
if (empty($formInfo['action'])) {
@unlink($cookieFile);
$ret = ['code' => 0, 'msg' => 'Scopus 页面结构已变化,未找到查询表单'];
if ($debug === 1) {
$ret['debug'] = $this->buildDebugInfo($lookupRes['url'], $lookupRes['http_code'], $lookupRes['body']);
}
return json($ret);
}
// 2) 组装查询参数(姓名 + 机构),并携带隐藏字段提交。
$postData = $formInfo['hidden_fields'];
$postData['authLast'] = $name;
$postData['affil'] = $affil;
$searchRes = $this->httpRequest($formInfo['action'], $postData, true, $lookupUrl, $cookieFile);
if (!$searchRes['ok']) {
@unlink($cookieFile);
$ret = ['code' => 0, 'msg' => '查询 Scopus 失败:' . $searchRes['msg']];
if ($debug === 1) {
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
}
return json($ret);
}
$blockMsg = $this->detectScopusBlocking($searchRes['body']);
if (!empty($blockMsg)) {
@unlink($cookieFile);
$ret = ['code' => 0, 'msg' => $blockMsg];
$fallback = $this->fallbackByOpenAlex($name, $affil);
if ($fallback !== null) {
$ret = array_merge($fallback, [
'msg' => $blockMsg . ',已自动降级 OpenAlex 结果'
]);
}
if ($debug === 1) {
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
}
return json($ret);
}
// 3) 从返回页提取 h-index优先匹配“h-index”关键词附近数字
$hIndex = $this->extractHIndexFromHtml($searchRes['body']);
if ($hIndex === null) {
@unlink($cookieFile);
$ret = [
'code' => 0,
'msg' => '未从 Scopus 结果页解析到 H 指数(可能需要人工登录或页面结构调整)'
];
if ($debug === 1) {
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
}
return json($ret);
}
@unlink($cookieFile);
$ret = [
'code' => 1,
'name' => $name,
'affil' => $affil,
'h_index_scopus' => $hIndex,
'source' => 'scopus_freelookup',
];
if ($debug === 1) {
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
}
return json($ret);
parent::__construct($request);
$this->bgService = new AuthorBackgroundService();
}
private function httpRequest($url, $postData = null, $followLocation = true, $referer = '', $cookieFile = '')
/**
* 作者背调 HTML 页面入口
*
* 1. 传了 ORCID → 直接生成报告
* 2. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID1 条直接报告,多条显示选择列表
*/
public function index()
{
$ch = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FOLLOWLOCATION => $followLocation,
CURLOPT_MAXREDIRS => 8,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
CURLOPT_ENCODING => '',
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
],
];
@set_time_limit(120);
if (!empty($referer)) {
$options[CURLOPT_REFERER] = $referer;
$formAction = $this->resolveFormAction();
$params = $this->resolveBackgroundParams();
$orcidNorm = $this->bgService->normalizeOrcid($params['orcid']);
if ($orcidNorm === ''
&& $params['last_name'] === ''
&& $params['first_name'] === ''
&& $params['institution'] === ''
) {
$this->assign('form_action', $formAction);
return $this->fetch('author/index');
}
if (!empty($cookieFile)) {
$options[CURLOPT_COOKIEJAR] = $cookieFile;
$options[CURLOPT_COOKIEFILE] = $cookieFile;
// 1. 有 ORCID → 直接报告页
if ($orcidNorm !== '') {
return $this->renderReportPage($params, $formAction);
}
if (is_array($postData)) {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = http_build_query($postData);
// 2. 无 ORCID → 姓氏必填,机构选填
if ($params['last_name'] === '') {
$this->assign([
'form_action' => $formAction,
'error_msg' => '未填 ORCID 时,请填写姓氏',
'last_name' => $params['last_name'],
'first_name' => $params['first_name'],
'institution' => $params['institution'],
]);
return $this->fetch('author/index');
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$error = curl_error($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$finalUrl = (string) curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
// 3. 仅按姓名搜 ORCID机构只做排序校验
$search = $this->bgService->searchOrcidCandidates(
$params['last_name'],
$params['first_name'],
$params['institution']
);
$candidates = $search['candidates'] ?? [];
if ($error) {
if (strpos($error, 'Maximum (') !== false && strpos($error, 'redirects followed') !== false) {
return [
'ok' => false,
'msg' => 'Scopus 跳转过多(可能触发登录/验证页面),请稍后重试或先在浏览器登录 Scopus',
'body' => '',
'http_code' => $httpCode,
'url' => $finalUrl
];
}
return ['ok' => false, 'msg' => $error, 'body' => '', 'http_code' => $httpCode, 'url' => $finalUrl];
if (empty($candidates)) {
return $this->renderOrcidRequiredPage($params, $formAction, '已在 OpenAlex、ORCID 官网、Scopus 按姓名检索,未找到带 ORCID 的作者');
}
if ($httpCode >= 400 || $httpCode === 0) {
return ['ok' => false, 'msg' => 'HTTP ' . $httpCode, 'body' => (string) $body, 'http_code' => $httpCode, 'url' => $finalUrl];
if (count($candidates) > 1) {
$this->assignCandidateListView($candidates, $params, $formAction);
return $this->fetch('author/select_orcid');
}
return ['ok' => true, 'msg' => '', 'body' => (string) $body, 'http_code' => $httpCode, 'url' => $finalUrl];
return $this->redirect($this->buildReportEntryUrl($formAction, $params, $candidates[0]['orcid']));
}
private function detectScopusBlocking($html)
/**
* 医学期刊作者背景调查报告ORCID 必填)
*
* POST/GET 参数:
* orcid / orcid_id ORCID必填
* lastName / last_name 姓(选填,用于 PubMed 辅助检索与报告展示)
* firstName / first_name 名(选填)
* institution / affiliation 机构(选填)
*/
public function background_report()
{
if (empty($html)) {
@set_time_limit(120);
$params = $this->resolveBackgroundParams();
$result = $this->bgService->buildReport(
$params['orcid'],
$params['last_name'],
$params['first_name'],
$params['institution']
);
if (empty($result['ok'])) {
$code = !empty($result['need_select']) ? 2 : 0;
return json([
'code' => $code,
'msg' => $result['msg'] ?? '查询失败',
'data' => $result['data'] ?? null,
]);
}
return json([
'code' => 1,
'msg' => 'success',
'data' => $result['data'],
]);
}
/** 与 background_report 相同(路由兼容) */
public function due_diligence()
{
return $this->background_report();
}
/**
* 解析背调查询参数(兼容多种命名)
*/
private function resolveBackgroundParams()
{
$pick = function (...$keys) {
foreach ($keys as $k) {
$v = trim((string) input('param.' . $k, ''));
if ($v === '') {
$v = trim((string) input('post.' . $k, ''));
}
if ($v === '') {
$v = trim((string) input('get.' . $k, ''));
}
if ($v !== '') {
return $v;
}
}
return '';
}
$text = strtolower(strip_tags($html));
if (strpos($text, 'sign in') !== false || strpos($text, 'institutional sign in') !== false) {
return 'Scopus 返回登录页,当前环境未授权访问作者详情页面';
}
if (strpos($text, 'captcha') !== false || strpos($text, 'are you a robot') !== false) {
return 'Scopus 触发了人机验证,当前接口无法自动通过';
}
return '';
}
private function buildDebugInfo($finalUrl, $httpCode, $html)
{
$normalized = html_entity_decode(strip_tags((string) $html), ENT_QUOTES, 'UTF-8');
$normalized = preg_replace('/\s+/u', ' ', $normalized);
$snippet = mb_substr($normalized, 0, 300, 'UTF-8');
};
return [
'final_url' => (string) $finalUrl,
'http_code' => (int) $httpCode,
'page_snippet' => $snippet,
'contains_signin' => stripos($normalized, 'sign in') !== false ? 1 : 0,
'contains_captcha' => stripos($normalized, 'captcha') !== false ? 1 : 0,
'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'),
];
}
private function extractScopusLookupForm($html)
private function resolveFormAction()
{
$ret = [
'action' => '',
'hidden_fields' => [],
];
if (empty($html)) {
return $ret;
}
// 优先定位包含 author 的 form减少解析误匹配。
if (preg_match('/<form[^>]*action=["\']([^"\']+)["\'][^>]*>.*?<\/form>/is', $html, $formMatch)) {
$action = trim($formMatch[1]);
if (!preg_match('/^https?:\/\//i', $action)) {
$action = 'https://www.scopus.com' . (substr($action, 0, 1) === '/' ? '' : '/') . $action;
}
$ret['action'] = $action;
if (preg_match_all('/<input[^>]*type=["\']hidden["\'][^>]*>/is', $formMatch[0], $inputs)) {
foreach ($inputs[0] as $inputTag) {
if (preg_match('/name=["\']([^"\']+)["\']/i', $inputTag, $nameMatch)) {
$fieldName = trim($nameMatch[1]);
$fieldVal = '';
if (preg_match('/value=["\']([^"\']*)["\']/i', $inputTag, $valMatch)) {
$fieldVal = $valMatch[1];
}
$ret['hidden_fields'][$fieldName] = $fieldVal;
}
}
}
}
return $ret;
return rtrim($this->request->root(), '/') . '/api/author/index';
}
private function extractHIndexFromHtml($html)
private function renderReportPage(array $params, $formAction)
{
if (empty($html)) {
return null;
}
$result = $this->bgService->buildReport(
$params['orcid'],
$params['last_name'],
$params['first_name'],
$params['institution']
);
$text = html_entity_decode(strip_tags($html), ENT_QUOTES, 'UTF-8');
$text = preg_replace('/\s+/u', ' ', $text);
$patterns = [
'/h[\-\s]?index[^0-9]{0,20}([0-9]{1,3})/iu',
'/([0-9]{1,3})[^0-9]{0,20}h[\-\s]?index/iu',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $text, $m)) {
return (int) $m[1];
if (empty($result['ok'])) {
$data = $result['data'] ?? [];
if (!empty($result['need_select'])) {
$this->assignCandidateListView($data['candidates'] ?? [], $params, $formAction);
return $this->fetch('author/select_orcid');
}
if (!empty($data['orcid_required'])) {
return $this->renderOrcidRequiredPage($params, $formAction, $data['hint'] ?? '');
}
$this->assign([
'form_action' => $formAction,
'error_msg' => $result['msg'] ?? '查询失败',
]);
return $this->fetch('author/index');
}
return null;
$this->assignReportView($result['data'], $formAction);
return $this->fetch('author/report');
}
private function fallbackByOpenAlex($name, $affil)
private function renderOrcidRequiredPage(array $params, $formAction, $hint = '')
{
$search = urlencode($name);
$url = "https://api.openalex.org/authors?search={$search}&limit=8";
$res = $this->httpRequest($url, null, true);
if (!$res['ok']) {
return null;
}
$data = json_decode($res['body'], true);
$list = $data['results'] ?? [];
if (empty($list)) {
return null;
}
$targetAffil = strtolower((string) $affil);
$match = null;
foreach ($list as $item) {
if (empty($targetAffil)) {
$match = $item;
break;
}
$insts = $item['affiliations'] ?? [];
foreach ($insts as $inst) {
$instName = strtolower($inst['display_name'] ?? '');
if ($instName !== '' && strpos($instName, $targetAffil) !== false) {
$match = $item;
break 2;
}
}
}
if ($match === null) {
$match = $list[0];
}
return [
'code' => 1,
'name' => $match['display_name'] ?? $name,
'affil' => !empty($match['affiliations'][0]['display_name']) ? $match['affiliations'][0]['display_name'] : $affil,
'h_index_scopus' => $match['summary_stats']['h_index_scopus'] ?? null,
'h_index_openalex' => $match['summary_stats']['h_index'] ?? null,
'source' => 'openalex_fallback',
];
$this->assign([
'form_action' => $formAction,
'submitted_name' => trim($params['first_name'] . ' ' . $params['last_name']),
'submitted_institution' => $params['institution'],
'last_name' => $params['last_name'],
'first_name' => $params['first_name'],
'institution' => $params['institution'],
'hint' => $hint,
]);
return $this->fetch('author/orcid_required');
}
}
private function buildReportEntryUrl($formAction, array $params, $orcid)
{
return $formAction . '?' . http_build_query(
array_filter([
'orcid' => $orcid,
'lastName' => $params['last_name'] ?? '',
'firstName' => $params['first_name'] ?? '',
'institution' => $params['institution'] ?? '',
], function ($v) {
return trim((string) $v) !== '';
}),
'',
'&',
PHP_QUERY_RFC3986
);
}
private function assignCandidateListView(array $candidates, array $params, $formAction)
{
foreach ($candidates as $idx => $item) {
$candidates[$idx]['report_url'] = $this->buildReportEntryUrl(
$formAction,
$params,
$item['orcid'] ?? ''
);
$candidates[$idx]['matched_class'] = !empty($item['institution_matched']) ? 'match' : '';
$name = trim((string) ($item['display_name'] ?? ''));
$candidates[$idx]['avatar_letter'] = $name !== ''
? mb_strtoupper(mb_substr($name, 0, 1))
: '?';
}
$this->assign([
'form_action' => $formAction,
'candidates' => $candidates,
'candidate_count' => count($candidates),
'submitted_name' => trim(($params['first_name'] ?? '') . ' ' . ($params['last_name'] ?? '')),
'submitted_institution' => $params['institution'] ?? '',
'last_name' => $params['last_name'] ?? '',
'first_name' => $params['first_name'] ?? '',
'institution' => $params['institution'] ?? '',
]);
}
private function assignReportView(array $report, $formAction)
{
$dupPaperCount = 0;
$duplicates = $report['duplicates'] ?? [];
foreach ($duplicates as $idx => $dg) {
$duplicates[$idx]['paper_count'] = count($dg['papers'] ?? []);
$dupPaperCount += $duplicates[$idx]['paper_count'];
foreach ($duplicates[$idx]['papers'] as $pi => $dp) {
$src = strtolower((string) ($dp['source'] ?? 'orcid'));
$duplicates[$idx]['papers'][$pi]['source_class'] = in_array($src, ['orcid', 'pubmed'], true) ? $src : 'orcid';
}
}
$report['duplicates'] = $duplicates;
$rw = $report['retraction_watch'] ?? [];
$items = $rw['items'] ?? [];
foreach ($items as $idx => $it) {
$title = !empty($it['author_title']) ? $it['author_title'] : ($it['title'] ?? '');
$items[$idx]['display_title'] = mb_substr($title, 0, 120);
$items[$idx]['reason_short'] = mb_substr((string) ($it['reason'] ?? ''), 0, 200);
$linkUrl = trim((string) ($it['url'] ?? ''));
if ($linkUrl === '') {
$linkUrl = 'https://retractionwatch.com/?s=' . rawurlencode((string) ($it['title'] ?? ''));
}
$items[$idx]['link_url'] = $linkUrl;
}
$report['retraction_watch']['items'] = $items;
$riskLevel = (string) ($report['conclusion']['risk_level'] ?? '');
$riskClass = 'risk-default';
if (strpos($riskLevel, '高风险') !== false) {
$riskClass = 'risk-high';
} elseif (strpos($riskLevel, '中风险') !== false) {
$riskClass = 'risk-mid';
} elseif (strpos($riskLevel, '低风险') !== false) {
$riskClass = 'risk-low';
}
$this->assign([
'form_action' => $formAction,
'report' => $report,
'risk_class' => $riskClass,
'orcid_affiliations_text' => implode('', $report['basic']['orcid_affiliations'] ?? []),
'openalex_institutions_text' => implode('', $report['basic']['openalex_institutions'] ?? []),
'topics_text' => implode('', $report['metrics']['topics'] ?? []),
'rw_match_total' => (int) ($rw['doi_match_count'] ?? 0)
+ (int) ($rw['name_match_count'] ?? 0)
+ (int) ($rw['name_loose_match_count'] ?? 0),
'dup_group_count' => count($duplicates),
'dup_paper_count' => $dupPaperCount,
'pubmed_list_count' => min(10, count($report['pubmed_papers'] ?? [])),
'orcid_section_num' => (($report['metrics']['pubmed_total'] ?? 0) > 0) ? '七' : '六',
]);
}
}

View File

@@ -0,0 +1,315 @@
<style>
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--primary-light: #dbeafe;
--accent: #0d9488;
--accent-light: #ccfbf1;
--bg: #f1f5f9;
--bg-gradient: linear-gradient(135deg, #eef2ff 0%, #f0fdfa 50%, #f8fafc 100%);
--card: #ffffff;
--text: #1e293b;
--text-muted: #64748b;
--border: #e2e8f0;
--radius: 14px;
--radius-sm: 8px;
--shadow: 0 4px 24px rgba(15, 23, 42, 0.06);
--shadow-hover: 0 8px 32px rgba(37, 99, 235, 0.12);
--success: #059669;
--success-bg: #d1fae5;
--warn: #d97706;
--warn-bg: #fef3c7;
--danger: #dc2626;
--danger-bg: #fee2e2;
}
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg-gradient);
min-height: 100vh;
margin: 0;
padding: 32px 20px 48px;
color: var(--text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
a { color: var(--primary); text-decoration: none; transition: color .15s; }
a:hover { color: var(--primary-dark); }
.page { max-width: 920px; margin: 0 auto; }
.page-narrow { max-width: 560px; margin: 0 auto; }
.page-form { max-width: 480px; margin: 0 auto; }
/* 顶栏 */
.topbar {
display: flex; align-items: center; gap: 10px;
margin-bottom: 20px; font-size: 14px; color: var(--text-muted);
}
.topbar a {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 14px; background: var(--card); border-radius: 999px;
border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,.04);
}
.topbar a:hover { border-color: var(--primary-light); background: #f8fafc; }
/* 卡片 */
.card {
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid rgba(255,255,255, .8);
padding: 32px 36px;
overflow: hidden;
}
.card-header {
text-align: center;
margin-bottom: 28px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.card-icon {
width: 56px; height: 56px; margin: 0 auto 14px;
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
border-radius: 16px;
display: flex; align-items: center; justify-content: center;
font-size: 26px; box-shadow: 0 4px 14px rgba(37, 99, 235, .25);
}
.card-title { font-size: 22px; font-weight: 700; color: var(--text); margin: 0 0 6px; letter-spacing: -.02em; }
.card-subtitle { font-size: 14px; color: var(--text-muted); margin: 0; }
/* 提示框 */
.alert {
padding: 12px 16px; border-radius: var(--radius-sm);
font-size: 13px; line-height: 1.65; margin-bottom: 20px;
}
.alert-info { background: var(--primary-light); color: #1e40af; border: 1px solid #bfdbfe; }
.alert-error { background: var(--danger-bg); color: #991b1b; border: 1px solid #fecaca; }
.alert-warn { background: var(--warn-bg); color: #92400e; border: 1px solid #fde68a; }
/* 表单 */
.form-group { margin-bottom: 18px; }
.form-label {
display: block; font-size: 13px; font-weight: 600;
color: var(--text); margin-bottom: 6px;
}
.form-label .opt { font-weight: 400; color: var(--text-muted); font-size: 12px; }
.form-input {
width: 100%; padding: 11px 14px;
border: 1px solid var(--border); border-radius: var(--radius-sm);
font-size: 15px; color: var(--text);
background: #fafbfc; transition: border-color .15s, box-shadow .15s, background .15s;
}
.form-input:focus {
outline: none; border-color: var(--primary);
background: #fff; box-shadow: 0 0 0 3px rgba(37, 99, 235, .12);
}
.form-input::placeholder { color: #94a3b8; }
.btn-primary {
width: 100%; margin-top: 8px; padding: 13px 20px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: #fff; border: none; border-radius: var(--radius-sm);
font-size: 16px; font-weight: 600; cursor: pointer;
box-shadow: 0 4px 14px rgba(37, 99, 235, .3);
transition: transform .15s, box-shadow .15s;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(37, 99, 235, .35);
}
.form-foot { font-size: 12px; color: var(--text-muted); margin-top: 20px; text-align: center; line-height: 1.7; }
/* 标签 */
.badge {
display: inline-block; padding: 3px 10px; border-radius: 999px;
font-size: 11px; font-weight: 700; letter-spacing: .02em;
}
.badge-success { background: var(--success-bg); color: var(--success); }
.badge-info { background: var(--primary-light); color: var(--primary-dark); }
.badge-warn { background: var(--warn-bg); color: var(--warn); }
.badge-danger { background: var(--danger-bg); color: var(--danger); }
/* 候选列表 */
.candidate-list { display: flex; flex-direction: column; gap: 12px; }
.candidate-card {
display: block; position: relative;
padding: 18px 20px 18px 72px;
border: 1px solid var(--border); border-radius: var(--radius-sm);
background: #fafbfc; text-decoration: none; color: inherit;
transition: border-color .2s, box-shadow .2s, transform .2s, background .2s;
}
.candidate-card:hover {
border-color: var(--primary); background: #fff;
box-shadow: var(--shadow-hover); transform: translateY(-2px);
}
.candidate-card.match {
border-color: #6ee7b7; background: linear-gradient(135deg, #f0fdf4 0%, #fff 100%);
}
.candidate-card.match:hover { border-color: var(--success); }
.candidate-avatar {
position: absolute; left: 18px; top: 50%; transform: translateY(-50%);
width: 44px; height: 44px; border-radius: 12px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #fff; font-size: 18px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.candidate-card.match .candidate-avatar {
background: linear-gradient(135deg, var(--success) 0%, #34d399 100%);
}
.candidate-name { font-size: 16px; font-weight: 700; color: var(--text); margin-bottom: 4px; padding-right: 100px; }
.candidate-orcid { font-size: 13px; color: var(--primary); font-family: ui-monospace, monospace; margin-bottom: 4px; }
.candidate-affil { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
.candidate-meta { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.candidate-action {
position: absolute; right: 18px; top: 50%; transform: translateY(-50%);
font-size: 13px; font-weight: 600; color: var(--primary);
display: flex; align-items: center; gap: 4px;
}
.candidate-card:hover .candidate-action { color: var(--primary-dark); }
/* 信息块 */
.info-panel {
background: #f8fafc; border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 16px 18px; margin-bottom: 22px;
}
.info-panel dt { font-size: 12px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; margin-top: 10px; }
.info-panel dt:first-child { margin-top: 0; }
.info-panel dd { margin: 4px 0 0; font-size: 15px; color: var(--text); }
/* 报告页 */
.report-hero {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 55%, #0d9488 100%);
color: #fff; margin: -32px -36px 28px; padding: 28px 36px 24px;
}
.report-hero h1 { font-size: 22px; margin: 0 0 12px; font-weight: 700; letter-spacing: -.02em; }
.report-meta { display: flex; flex-wrap: wrap; gap: 8px; }
.report-chip {
font-size: 12px; padding: 5px 12px; border-radius: 999px;
background: rgba(255,255,255,.15); backdrop-filter: blur(4px);
border: 1px solid rgba(255,255,255,.2);
}
.risk-banner {
padding: 16px 20px; border-radius: var(--radius-sm); margin-bottom: 24px;
font-size: 15px; font-weight: 600; display: flex; align-items: flex-start; gap: 12px;
}
.risk-banner::before { font-size: 22px; line-height: 1; }
.risk-low { background: var(--success-bg); color: #065f46; border: 1px solid #a7f3d0; }
.risk-low::before { content: "✓"; }
.risk-mid { background: var(--warn-bg); color: #92400e; border: 1px solid #fde68a; }
.risk-mid::before { content: "⚠"; }
.risk-high { background: var(--danger-bg); color: #991b1b; border: 1px solid #fecaca; }
.risk-high::before { content: "✕"; }
.risk-default { background: #f1f5f9; color: var(--text); border: 1px solid var(--border); }
.risk-default::before { content: ""; }
.sec-title {
font-size: 16px; font-weight: 700; color: var(--text);
margin: 28px 0 14px; padding-left: 14px;
border-left: 4px solid var(--primary);
display: flex; align-items: center; gap: 8px;
}
.sec-num {
display: inline-flex; width: 24px; height: 24px; border-radius: 6px;
background: var(--primary-light); color: var(--primary);
font-size: 12px; font-weight: 800; align-items: center; justify-content: center;
}
.data-table {
width: 100%; border-collapse: separate; border-spacing: 0;
font-size: 14px; margin-bottom: 8px;
border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden;
}
.data-table th, .data-table td {
padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border);
}
.data-table tr:last-child th, .data-table tr:last-child td { border-bottom: none; }
.data-table th {
background: #f8fafc; width: 30%; font-weight: 600;
color: var(--text-muted); font-size: 13px;
}
.data-table td { color: var(--text); }
.data-table tr:hover td { background: #fafbfc; }
.tag { display: inline-block; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 700; }
.tag-ok { background: var(--success-bg); color: var(--success); }
.tag-warn { background: var(--warn-bg); color: var(--warn); }
.tag-bad { background: var(--danger-bg); color: var(--danger); }
.paper-item { font-size: 14px; padding: 10px 0; border-bottom: 1px dashed var(--border); line-height: 1.65; }
.paper-item:last-child { border-bottom: none; }
.paper-link { color: var(--primary); font-weight: 500; }
.paper-link:hover { text-decoration: underline; }
.paper-meta { color: var(--text-muted); font-size: 12px; margin-left: 6px; }
.dup-card {
margin: 12px 0; padding: 14px 16px;
background: #fffbeb; border: 1px solid #fde68a;
border-left: 4px solid var(--warn); border-radius: var(--radius-sm);
}
.src-tag {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 11px; font-weight: 700; margin-right: 4px;
}
.src-orcid { background: var(--accent-light); color: #0f766e; }
.src-pubmed { background: var(--primary-light); color: var(--primary-dark); }
.match-doi { background: var(--success-bg); color: var(--success); }
.match-name { background: var(--warn-bg); color: var(--warn); }
.match-loose { background: var(--danger-bg); color: var(--danger); }
.btn-scopus {
display: inline-flex; align-items: center; gap: 8px; margin-top: 10px;
padding: 12px 22px; background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
color: #fff !important; border-radius: var(--radius-sm);
font-size: 14px; font-weight: 700; text-decoration: none;
box-shadow: 0 4px 14px rgba(234, 88, 12, .25);
transition: transform .15s, box-shadow .15s;
}
.btn-scopus:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(234, 88, 12, .3); color: #fff !important; }
a.ext { color: var(--primary); font-weight: 500; }
a.ext:hover { text-decoration: underline; }
.report-notes {
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.75;
}
.report-notes-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
margin: 0 0 10px;
}
.report-notes ul {
margin: 0;
padding-left: 18px;
}
.report-notes li {
margin: 6px 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;
}
.stat-row { display: flex; flex-wrap: wrap; gap: 12px; margin: 16px 0 8px; }
.stat-box {
flex: 1; min-width: 120px; padding: 14px 16px;
background: #f8fafc; border: 1px solid var(--border); border-radius: var(--radius-sm);
text-align: center;
}
.stat-box strong { display: block; font-size: 24px; color: var(--primary); font-weight: 800; line-height: 1.2; }
.stat-box span { font-size: 12px; color: var(--text-muted); }
@media (max-width: 640px) {
body { padding: 16px 12px 32px; }
.card { padding: 24px 20px; }
.report-hero { margin: -24px -20px 20px; padding: 22px 20px; }
.candidate-card { padding-left: 18px; padding-top: 52px; }
.candidate-avatar { top: 16px; left: 18px; transform: none; }
.candidate-name { padding-right: 0; }
.candidate-action { top: auto; bottom: 16px; right: 18px; transform: none; }
.data-table th, .data-table td { padding: 10px 12px; font-size: 13px; }
}
</style>

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>作者背调查询</title>
{include file="author/_styles" /}
</head>
<body>
<div class="page-form">
<div class="card">
<div class="card-header">
<div class="card-icon">📋</div>
<h1 class="card-title">医学期刊 · 作者背景调查</h1>
<p class="card-subtitle">青年编委 / 特约审稿人 / 作者资质初审</p>
</div>
<div class="alert alert-info">
填写 <strong>ORCID</strong> 可直接生成报告;未填 ORCID 时<strong>姓氏必填</strong>、机构选填,系统按姓名检索后供您选择。
</div>
{notempty name="error_msg"}
<div class="alert alert-error">{$error_msg|htmlspecialchars}</div>
{/notempty}
<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}">
</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}">
</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}">
</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}">
</div>
<button class="btn-primary" type="submit">生成背调报告</button>
</form>
<p class="form-foot">
作者可在 <a href="https://orcid.org/" target="_blank" rel="noopener">orcid.org</a> 注册或查询自己的 ORCID iD
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>缺少 ORCID</title>
{include file="author/_styles" /}
</head>
<body>
<div class="page-narrow">
<div class="topbar" style="justify-content:center;">
<a href="{$form_action}">← 返回查询页</a>
</div>
<div class="card">
<div class="card-header">
<div class="card-icon" style="background:linear-gradient(135deg,#f59e0b 0%,#ef4444 100%);"></div>
<h1 class="card-title" style="color:#dc2626;">无法自动匹配 ORCID</h1>
<p class="card-subtitle">请手动补充 ORCID 后重新生成报告</p>
</div>
<div class="alert alert-warn">
系统已按 <strong>姓名</strong> 在 OpenAlex、ORCID 官网、Scopus 检索,未找到可用结果。
{notempty name="hint"}<br><br>{$hint|htmlspecialchars}{/notempty}
</div>
{notempty name="submitted_name"}
<dl class="info-panel">
<dt>已提交姓名</dt>
<dd>{$submitted_name|htmlspecialchars}</dd>
{notempty name="submitted_institution"}
<dt>已提交机构</dt>
<dd>{$submitted_institution|htmlspecialchars}</dd>
{/notempty}
</dl>
{/notempty}
<form method="get" action="{$form_action}">
<div class="form-group">
<label class="form-label">请填写 ORCID <span style="color:#dc2626">*</span></label>
<input class="form-input" type="text" name="orcid" placeholder="0000-0002-6388-7847" required autofocus>
</div>
<input type="hidden" name="lastName" value="{$last_name|htmlspecialchars}">
<input type="hidden" name="firstName" value="{$first_name|htmlspecialchars}">
<input type="hidden" name="institution" value="{$institution|htmlspecialchars}">
<button class="btn-primary" type="submit">补充 ORCID 并重新生成</button>
</form>
<p class="form-foot">
<a href="https://orcid.org/orcid-search/search" target="_blank" rel="noopener">在 ORCID 官网查找 ↗</a>
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>作者背调报告 · {$report.basic.display_name|htmlspecialchars}</title>
{include file="author/_styles" /}
</head>
<body>
<div class="page">
<div class="topbar">
<a href="{$form_action}">← 返回查询页</a>
</div>
<div class="card">
<div class="report-hero">
<h1>作者背景调查报告</h1>
<div class="report-meta">
<span class="report-chip">{$report.basic.display_name|htmlspecialchars}</span>
{notempty name="report.query.orcid"}
<span class="report-chip">ORCID {$report.query.orcid|htmlspecialchars}</span>
{/notempty}
<span class="report-chip">{$report.query.way|htmlspecialchars}</span>
<span class="report-chip">{$report.report_at|htmlspecialchars}</span>
</div>
</div>
<div class="risk-banner {$risk_class}">
<div>{$report.conclusion.risk_level|htmlspecialchars}</div>
</div>
<div class="sec-title"><span class="sec-num">1</span>学术指标</div>
<div class="stat-row">
<div class="stat-box"><strong>{$report.metrics.works_count}</strong><span>论文总数</span></div>
<div class="stat-box"><strong>{$report.metrics.cited_by_count}</strong><span>总被引</span></div>
<div class="stat-box"><strong>{$report.metrics.h_index}</strong><span>H 指数</span></div>
<div class="stat-box"><strong>{$report.metrics.i10_index}</strong><span>i10 指数</span></div>
<div class="stat-box"><strong>{$report.metrics.pubmed_total}</strong><span>PubMed</span></div>
</div>
<table class="data-table">
{notempty name="report.metrics.pubmed_query"}
<tr><th>PubMed 检索式</th><td>
{$report.metrics.pubmed_query|htmlspecialchars}
{gt name="report.metrics.pubmed_total" value="0"}
<a class="ext" href="{$report.metrics.pubmed_url|htmlspecialchars}" target="_blank" rel="noopener">在 PubMed 打开 ↗</a>
{/gt}
</td></tr>
{/notempty}
{notempty name="report.metrics.topics"}
<tr><th>主要研究方向</th><td>{$topics_text|htmlspecialchars}</td></tr>
{/notempty}
</table>
<div class="sec-title"><span class="sec-num">2</span>诚信 / 不良记录</div>
<div class="alert alert-info" style="font-size:13px;">
<strong>比对策略:</strong>
① 有 DOI 的 ORCID 作品 → 撤稿库 DOI 精确比对 &nbsp;|&nbsp;
② 无 DOI → 姓名+题目回退匹配(同名有风险)<br>
已比对 DOI <strong>{$report.retraction_watch.checked_doi_count|default=0}</strong> 篇 ·
无 DOI <strong>{$report.retraction_watch.no_doi_count|default=0}</strong>
{gt name="rw_match_total" value="0"}
· 命中:
<span class="src-tag match-doi">DOI {$report.retraction_watch.doi_match_count|default=0}</span>
<span class="src-tag match-name">姓名+题目 {$report.retraction_watch.name_match_count|default=0}</span>
<span class="src-tag match-loose">姓名+机构 {$report.retraction_watch.name_loose_match_count|default=0}</span>
{/gt}
</div>
{empty name="report.retraction_watch.ok"}
<p><span class="tag tag-warn">⚠ {$report.retraction_watch.msg|default='撤稿数据库暂不可用'|htmlspecialchars}</span></p>
{else/}
{empty name="report.retraction_watch.items"}
<p><span class="tag tag-ok">✅ 未匹配到撤稿 / 关注声明记录</span></p>
{else/}
<p style="margin-bottom:14px;">
{gt name="report.retraction_watch.misconduct_count" value="0"}
<span class="tag tag-bad">发现不端相关记录</span>
{else/}
<span class="tag tag-warn">发现撤稿 / 关注声明</span>
{/gt}
&nbsp;撤稿 {$report.retraction_watch.retraction_count|default=0} 条 · 不端 {$report.retraction_watch.misconduct_count|default=0} 条
</p>
<ul style="list-style:none;padding:0;margin:0;">
{volist name="report.retraction_watch.items" id="it"}
<li class="paper-item" style="padding:14px 0;">
{if condition="$it['match_type'] eq 'doi'"}
<span class="src-tag match-doi">{$it.match_label|htmlspecialchars}</span>
{elseif condition="$it['match_type'] eq 'name'"/}
<span class="src-tag match-name">{$it.match_label|htmlspecialchars}</span>
{else/}
<span class="src-tag match-loose">{$it.match_label|htmlspecialchars}</span>
{/if}
<strong>{$it.nature|htmlspecialchars}</strong>
<a class="paper-link" href="{$it.link_url|htmlspecialchars}" target="_blank" rel="noopener">{$it.display_title|htmlspecialchars} ↗</a>
{notempty name="it.misconduct"}<em style="color:#dc2626;"> [不端]</em>{/notempty}
<br><small style="color:var(--text-muted);">
{notempty name="it.doi"}DOI <a class="ext" href="https://doi.org/{$it.doi|htmlspecialchars}" target="_blank">{$it.doi|htmlspecialchars}</a> · {/notempty}
{$it.reason_short|htmlspecialchars} · {$it.date|htmlspecialchars}
</small>
</li>
{/volist}
</ul>
{/empty}
{/empty}
<div class="sec-title"><span class="sec-num">3</span>基本信息</div>
<table class="data-table">
<tr><th>姓名</th><td><strong>{$report.basic.display_name|htmlspecialchars}</strong></td></tr>
<tr><th>ORCID</th><td><a class="ext" href="{$report.basic.orcid_url|htmlspecialchars}" target="_blank" rel="noopener">{$report.basic.orcid|htmlspecialchars} ↗</a></td></tr>
{notempty name="report.basic.orcid_affiliations"}
<tr><th>ORCID 单位</th><td>{$orcid_affiliations_text|htmlspecialchars}</td></tr>
{/notempty}
{notempty name="report.basic.openalex_institutions"}
<tr><th>OpenAlex 机构</th><td>{$openalex_institutions_text|htmlspecialchars}</td></tr>
{/notempty}
{notempty name="report.basic.openalex_url"}
<tr><th>OpenAlex 档案</th><td><a class="ext" href="{$report.basic.openalex_url|htmlspecialchars}" target="_blank" rel="noopener">查看学者页面 ↗</a></td></tr>
{/notempty}
{notempty name="report.basic.scopus_url"}
<tr><th>Scopus 作者页</th><td>
<a class="ext" href="{$report.basic.scopus_url|htmlspecialchars}" target="_blank" rel="noopener">
打开 Scopus 档案ID: {$report.basic.scopus_id|htmlspecialchars})↗
</a>
</td></tr>
{/notempty}
</table>
<div class="sec-title"><span class="sec-num">4</span>Scopus 检索</div>
{notempty name="report.scopus.search_url"}
<p style="font-size:14px;color:var(--text-muted);margin:0 0 10px;">
检索方式:{notempty name="report.query.orcid"}ORCID {$report.query.orcid|htmlspecialchars}{else/}{$report.query.first_name|htmlspecialchars} {$report.query.last_name|htmlspecialchars} · {$report.query.institution|htmlspecialchars}{/notempty}
</p>
<a class="btn-scopus" href="{$report.scopus.search_url|htmlspecialchars}" target="_blank" rel="noopener">打开 Scopus 作者检索 ↗</a>
<p style="font-size:12px;color:var(--text-muted);margin-top:10px;">需机构 Scopus 订阅账号登录后查看完整数据</p>
{else/}
<div class="alert alert-warn">请填写 ORCID或填写姓名与机构后生成 Scopus 链接。</div>
{/notempty}
{notempty name="report.scopus.api.ok"}
<p style="margin-top:18px;font-size:14px;"><strong>Scopus API</strong> {$report.scopus.api.msg|htmlspecialchars}</p>
{notempty name="report.scopus.api.entries"}
<table class="data-table" style="margin-top:10px;">
<tr>
<th>姓名</th><th>机构</th><th>论文</th><th>被引</th><th>H指数</th><th>操作</th>
</tr>
{volist name="report.scopus.api.entries" id="se"}
<tr>
<td>{$se.name|htmlspecialchars}</td>
<td>{$se.affiliation|htmlspecialchars}</td>
<td>{$se.document_count}</td>
<td>{$se.cited_by_count}</td>
<td>{$se.h_index}</td>
<td>
{notempty name="se.url"}
<a class="ext" href="{$se.url|htmlspecialchars}" target="_blank" rel="noopener">进入 ↗</a>
{else/}—{/notempty}
</td>
</tr>
{/volist}
</table>
{/notempty}
{else/}
<p style="font-size:13px;color:var(--text-muted);margin-top:12px;">{$report.scopus.api.msg|default='未配置 Scopus API Key'|htmlspecialchars}</p>
{/notempty}
<div class="sec-title"><span class="sec-num">5</span>本地辅助检测</div>
<p>
题目重复:
{empty name="report.duplicates"}
<span class="tag tag-ok">未发现</span>
{else/}
<span class="tag tag-warn">{$dup_group_count} 组重复(共 {$dup_paper_count} 篇)</span>
{/empty}
</p>
{notempty name="report.duplicates"}
{volist name="report.duplicates" id="dg" key="gi"}
<div class="dup-card">
<strong>第 {$gi} 组:</strong>{$dg.title|htmlspecialchars}
<small style="color:var(--text-muted);">{$dg.paper_count} 篇相同)</small>
<ul style="margin:10px 0 0;padding-left:18px;">
{volist name="dg.papers" id="dp" key="pi"}
<li class="paper-item" style="border:none;padding:6px 0;">
<span class="src-tag src-{$dp.source_class}">{$dp.source|default='未知'|htmlspecialchars}</span>
[{$dp.year|htmlspecialchars}]
{notempty name="dp.open_url"}
<a class="paper-link" href="{$dp.open_url|htmlspecialchars}" target="_blank" rel="noopener">{$dp.title|htmlspecialchars} ↗</a>
{else/}{$dp.title|htmlspecialchars}{/notempty}
{notempty name="dp.journal"} <em style="color:var(--text-muted);">— {$dp.journal|htmlspecialchars}</em>{/notempty}
</li>
{/volist}
</ul>
</div>
{/volist}
{/notempty}
{gt name="report.metrics.pubmed_total" value="0"}
<div class="sec-title"><span class="sec-num">6</span>PubMed 文献({$report.metrics.pubmed_total} 篇)</div>
{empty name="report.pubmed_papers"}
<p style="color:var(--text-muted);font-size:14px;">未获取到文献详情,请点击上方 PubMed 链接查看。</p>
{else/}
{volist name="report.pubmed_papers" id="p" key="i" length="10"}
<div class="paper-item">{$i}. [{$p.year|htmlspecialchars}]
{notempty name="p.open_url"}
<a class="paper-link" href="{$p.open_url|htmlspecialchars}" target="_blank" rel="noopener">{$p.title|htmlspecialchars} ↗</a>
{else/}{$p.title|htmlspecialchars}{/notempty}
{notempty name="p.journal"} <em style="color:var(--text-muted);">— {$p.journal|htmlspecialchars}</em>{/notempty}
<span class="paper-meta">
{notempty name="p.pmid"}<a class="ext" href="https://pubmed.ncbi.nlm.nih.gov/{$p.pmid|htmlspecialchars}/" target="_blank">PubMed</a>{/notempty}
{notempty name="p.doi"} · <a class="ext" href="https://doi.org/{$p.doi|htmlspecialchars}" target="_blank">DOI</a>{/notempty}
</span>
</div>
{/volist}
{/empty}
{/gt}
{notempty name="report.orcid_papers.papers"}
<div class="sec-title"><span class="sec-num">{$orcid_section_num}</span>ORCID 作品({$report.orcid_papers.total} 条)</div>
{volist name="report.orcid_papers.papers" id="p" key="i" length="10"}
<div class="paper-item">{$i}. [{$p.year|htmlspecialchars}]
{notempty name="p.open_url"}
<a class="paper-link" href="{$p.open_url|htmlspecialchars}" target="_blank" rel="noopener">{$p.title|htmlspecialchars} ↗</a>
{else/}{$p.title|htmlspecialchars}{/notempty}
{notempty name="p.journal"} <em style="color:var(--text-muted);">— {$p.journal|htmlspecialchars}</em>{/notempty}
<span class="paper-meta">
{notempty name="p.doi"}<a class="ext" href="https://doi.org/{$p.doi|htmlspecialchars}" target="_blank">DOI</a>{/notempty}
{notempty name="p.pmid"} · <a class="ext" href="https://pubmed.ncbi.nlm.nih.gov/{$p.pmid|htmlspecialchars}/" target="_blank">PubMed</a>{/notempty}
</span>
</div>
{/volist}
{/notempty}
{notempty name="report.conclusion.notes"}
<div class="report-notes">
<p class="report-notes-title">说明</p>
<ul>
{volist name="report.conclusion.notes" id="note"}
<li>{$note|htmlspecialchars}</li>
{/volist}
</ul>
</div>
{/notempty}
<p class="report-foot">
数据来源OpenAlex · ORCID · PubMed · Scopus · Retraction Watch<br>
适用于青年编委 / 特约审稿人 / 作者资质初审
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>选择作者 ORCID</title>
{include file="author/_styles" /}
</head>
<body>
<div class="page">
<div class="topbar">
<a href="{$form_action}">← 返回查询页</a>
</div>
<div class="card">
<div class="card-header" style="text-align:left;border-bottom:none;padding-bottom:0;margin-bottom:20px;">
<h1 class="card-title" style="text-align:left;">匹配到 {$candidate_count} 位作者</h1>
<p class="card-subtitle" style="text-align:left;margin-top:8px;">
检索姓名:<strong>{$submitted_name|htmlspecialchars}</strong>
{notempty name="submitted_institution"} · 参考机构:<strong>{$submitted_institution|htmlspecialchars}</strong>{/notempty}
</p>
</div>
<div class="alert alert-info">
已按<strong>姓名</strong>检索 ORCID。若填写了机构<span class="badge badge-success">机构一致</span> 的候选项排在最前,请点击确认后生成报告。
</div>
<div class="candidate-list">
{volist name="candidates" id="item"}
<a class="candidate-card {$item.matched_class}" href="{$item.report_url|htmlspecialchars}">
<div class="candidate-avatar">{$item.avatar_letter|htmlspecialchars}</div>
<span class="candidate-action">生成报告 →</span>
<div class="candidate-name">{$item.display_name|default='(姓名未知)'|htmlspecialchars}</div>
<div class="candidate-orcid">{$item.orcid|htmlspecialchars}</div>
{notempty name="item.affiliations_text"}
<div class="candidate-affil">{$item.affiliations_text|htmlspecialchars}</div>
{/notempty}
<div class="candidate-meta">
{eq name="item.institution_matched" value="1"}<span class="badge badge-success">机构一致</span>{/eq}
{notempty name="item.sources_text"}<span class="badge badge-info">{$item.sources_text|htmlspecialchars}</span>{/notempty}
</div>
</a>
{/volist}
</div>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
return [
'host' => '127.0.0.1',
'port' => 5672,
'user' => 'admin',
'password' => '751019',
'user' => 'guest',//admin
'password' => 'guest',//'751019'
'vhost' => '/',
'reference_check' => [

View File

@@ -18,4 +18,14 @@ return [
':name' => ['index/hello', ['method' => 'post']],
],
// Author 背调(兼容 /api/author/ 与 /api/Author/
'api/author/index' => 'api/Author/index',
'api/author/background_report' => 'api/Author/background_report',
'api/author/due_diligence' => 'api/Author/due_diligence',
'api/Author/index' => 'api/Author/index',
'api/Author/background_report' => 'api/Author/background_report',
'api/Author/backgroundReport' => 'api/Author/background_report',
'api/Author/due_diligence' => 'api/Author/due_diligence',
'api/Author/dueDiligence' => 'api/Author/due_diligence',
];