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) ? '七' : '六', ]); } }