395 lines
13 KiB
PHP
395 lines
13 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\service\AuthorBackgroundService;
|
||
use think\Controller;
|
||
|
||
/**
|
||
* 作者背调 API(前后端分离,返回 JSON)
|
||
*
|
||
* 主接口:background_report / due_diligence(ORCID 必填)
|
||
* Scopus 相关接口委托 AuthorInfo 实现
|
||
*/
|
||
class Author extends Controller
|
||
{
|
||
/** @var AuthorBackgroundService */
|
||
private $bgService;
|
||
|
||
/** @var AuthorInfo */
|
||
private $authorInfo;
|
||
|
||
public function __construct(\think\Request $request = null)
|
||
{
|
||
parent::__construct($request);
|
||
$this->bgService = new AuthorBackgroundService();
|
||
$this->authorInfo = new AuthorInfo();
|
||
}
|
||
|
||
/**
|
||
* 作者背调 HTML 页面入口
|
||
*
|
||
* 1. 传了 ORCID → 直接生成报告
|
||
* 2. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID;1 条直接报告,多条显示选择列表
|
||
*/
|
||
public function index()
|
||
{
|
||
@set_time_limit(120);
|
||
|
||
$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');
|
||
}
|
||
|
||
// 1. 有 ORCID → 直接报告页
|
||
if ($orcidNorm !== '') {
|
||
return $this->renderReportPage($params, $formAction);
|
||
}
|
||
|
||
// 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');
|
||
}
|
||
|
||
// 3. 仅按姓名搜 ORCID(机构只做排序校验)
|
||
$search = $this->bgService->searchOrcidCandidates(
|
||
$params['last_name'],
|
||
$params['first_name'],
|
||
$params['institution']
|
||
);
|
||
$candidates = $search['candidates'] ?? [];
|
||
|
||
if (empty($candidates)) {
|
||
return $this->renderOrcidRequiredPage($params, $formAction, '已在 OpenAlex、ORCID 官网、Scopus 按姓名检索,未找到带 ORCID 的作者');
|
||
}
|
||
|
||
if (count($candidates) > 1) {
|
||
$this->assignCandidateListView($candidates, $params, $formAction);
|
||
return $this->fetch('author/select_orcid');
|
||
}
|
||
|
||
return $this->redirect($this->buildReportEntryUrl($formAction, $params, $candidates[0]['orcid']));
|
||
}
|
||
|
||
/**
|
||
* 医学期刊作者背景调查报告(ORCID 必填)
|
||
*
|
||
* POST/GET 参数:
|
||
* orcid / orcid_id ORCID(必填)
|
||
* lastName / last_name 姓(选填,用于 PubMed 辅助检索与报告展示)
|
||
* firstName / first_name 名(选填)
|
||
* institution / affiliation 机构(选填)
|
||
*/
|
||
public function background_report()
|
||
{
|
||
@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'],
|
||
]);
|
||
}
|
||
|
||
/** camelCase 别名 */
|
||
public function backgroundReport()
|
||
{
|
||
return $this->background_report();
|
||
}
|
||
|
||
/** 与 background_report 相同(路由兼容) */
|
||
public function due_diligence()
|
||
{
|
||
return $this->background_report();
|
||
}
|
||
|
||
public function dueDiligence()
|
||
{
|
||
return $this->due_diligence();
|
||
}
|
||
|
||
/**
|
||
* OpenAlex + Crossref 诚信扫描(不依赖 ORCID 必填)
|
||
*/
|
||
public function background_check()
|
||
{
|
||
return $this->authorInfo->background_check();
|
||
}
|
||
|
||
public function backgroundCheck()
|
||
{
|
||
return $this->background_check();
|
||
}
|
||
|
||
public function get_hindex()
|
||
{
|
||
return $this->authorInfo->get_hindex();
|
||
}
|
||
|
||
public function getHindex()
|
||
{
|
||
return $this->get_hindex();
|
||
}
|
||
|
||
public function get_scopus_id()
|
||
{
|
||
return $this->authorInfo->get_scopus_id();
|
||
}
|
||
|
||
public function getScopusId()
|
||
{
|
||
return $this->get_scopus_id();
|
||
}
|
||
|
||
public function check_scopus_cookie()
|
||
{
|
||
return $this->authorInfo->check_scopus_cookie();
|
||
}
|
||
|
||
public function checkScopusCookie()
|
||
{
|
||
return $this->check_scopus_cookie();
|
||
}
|
||
|
||
public function save_scopus_cookie()
|
||
{
|
||
return $this->authorInfo->save_scopus_cookie();
|
||
}
|
||
|
||
public function saveScopusCookie()
|
||
{
|
||
return $this->save_scopus_cookie();
|
||
}
|
||
|
||
public function login_scopus()
|
||
{
|
||
return $this->authorInfo->login_scopus();
|
||
}
|
||
|
||
public function loginScopus()
|
||
{
|
||
return $this->login_scopus();
|
||
}
|
||
|
||
public function check_elsevier_api()
|
||
{
|
||
if (method_exists($this->authorInfo, 'check_elsevier_api')) {
|
||
return $this->authorInfo->check_elsevier_api();
|
||
}
|
||
return json(['code' => 0, 'msg' => 'check_elsevier_api not implemented']);
|
||
}
|
||
|
||
public function checkElsevierApi()
|
||
{
|
||
return $this->check_elsevier_api();
|
||
}
|
||
|
||
/**
|
||
* 解析背调查询参数(兼容多种命名)
|
||
*/
|
||
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 '';
|
||
};
|
||
|
||
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'),
|
||
];
|
||
}
|
||
|
||
private function resolveFormAction()
|
||
{
|
||
return rtrim($this->request->root(), '/') . '/api/author/index';
|
||
}
|
||
|
||
private function renderReportPage(array $params, $formAction)
|
||
{
|
||
$result = $this->bgService->buildReport(
|
||
$params['orcid'],
|
||
$params['last_name'],
|
||
$params['first_name'],
|
||
$params['institution']
|
||
);
|
||
|
||
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');
|
||
}
|
||
|
||
$this->assignReportView($result['data'], $formAction);
|
||
return $this->fetch('author/report');
|
||
}
|
||
|
||
private function renderOrcidRequiredPage(array $params, $formAction, $hint = '')
|
||
{
|
||
$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) ? '七' : '六',
|
||
]);
|
||
}
|
||
}
|