450 lines
16 KiB
PHP
450 lines
16 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\service\AuthorBackgroundService;
|
||
use think\Controller;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 作者背调:HTML 报告页 + JSON API
|
||
*
|
||
* 主接口:index / background_report / due_diligence
|
||
*/
|
||
class Author extends Controller
|
||
{
|
||
/** @var AuthorBackgroundService */
|
||
private $bgService;
|
||
|
||
/** @var string */
|
||
private $articleLookupError = '';
|
||
|
||
public function __construct(\think\Request $request = null)
|
||
{
|
||
parent::__construct($request);
|
||
$this->bgService = new AuthorBackgroundService();
|
||
}
|
||
|
||
/**
|
||
* 作者背调 HTML 页面入口
|
||
*
|
||
* 1. 传了 ORCID → 直接生成报告
|
||
* 2. 传了 articleId(稿件作者 ID,即 art_aut_id)→ 从 t_article_author 补全 ORCID / 姓名 / 机构
|
||
* 3. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID;1 条直接报告,多条显示选择列表
|
||
*/
|
||
public function index()
|
||
{
|
||
@set_time_limit(120);
|
||
|
||
$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 === ''
|
||
&& $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'],
|
||
]);
|
||
}
|
||
|
||
/** 与 background_report 相同(路由兼容) */
|
||
public function due_diligence()
|
||
{
|
||
return $this->background_report();
|
||
}
|
||
|
||
/**
|
||
* 解析背调查询参数(兼容多种命名)
|
||
*/
|
||
private function resolveBackgroundParams()
|
||
{
|
||
$this->articleLookupError = '';
|
||
|
||
$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 '';
|
||
};
|
||
|
||
$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' => $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';
|
||
}
|
||
|
||
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) ? '七' : '六',
|
||
]);
|
||
}
|
||
}
|