bgService = new AuthorBackgroundService(); } /** * 作者背调 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'], ]); } /** 与 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 ''; }; 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) ? '七' : '六', ]); } }