bgCheck = new BackgroundCheckService(); $this->refUtil = new ReferenceCheckService(); $this->crossref = new CrossrefService([ 'mailto' => trim((string)\think\Env::get('crossref_mailto', '')), ]); } /** * 本文作者身份(精准统计仅认 ORCID → OpenAlex / ORCID 键) * * @return array */ public function resolveManuscriptAuthors($pArticleId) { $pArticleId = intval($pArticleId); if ($pArticleId <= 0) { return []; } $rows = Db::name('production_article_author') ->field('p_article_author_id,first_name,last_name,author_name,orcid') ->where('p_article_id', $pArticleId) ->where('state', 0) ->select(); $identities = []; foreach ($rows as $row) { $identities[] = $this->resolveOneManuscriptAuthor($row); } return $identities; } /** * 单条参考文献的作者身份(优先 OpenAlex work by DOI,其次 Crossref ORCID) * * @return array{ * authorships:array, * first_author:array|null, * match_confidence:string, * identity_source:string * } */ public function resolveReferAuthorships(array $refer, array &$workCache, array &$crossrefCache) { $pReferId = intval($refer['p_refer_id'] ?? 0); if ($pReferId > 0) { $stored = (new ReferenceReferAuthorService())->loadAuthorshipsByPReferId($pReferId); if (!empty($stored)) { return [ 'authorships' => $stored, 'first_author' => $this->pickFirstAuthorship($stored), 'match_confidence' => self::MATCH_PRECISE, 'identity_source' => 'stored', ]; } } $doi = $this->refUtil->extractDoiFromRefer($refer); if ($doi === '') { return [ 'authorships' => [], 'first_author' => null, 'match_confidence' => self::MATCH_NONE, 'identity_source' => '', ]; } if (!array_key_exists($doi, $workCache)) { $workCache[$doi] = $this->fetchOpenAlexAuthorships($doi); usleep(80000); } $authorships = $workCache[$doi]; $source = 'openalex'; if (empty($authorships)) { if (!array_key_exists($doi, $crossrefCache)) { $crossrefCache[$doi] = $this->fetchCrossrefAuthorships($doi); usleep(80000); } $authorships = $crossrefCache[$doi]; $source = 'crossref_orcid'; } $firstAuthor = $this->pickFirstAuthorship($authorships); $confidence = empty($authorships) ? self::MATCH_NONE : self::MATCH_PRECISE; return [ 'authorships' => $authorships, 'first_author' => $firstAuthor, 'match_confidence' => $confidence, 'identity_source' => $source, ]; } /** * @param array $manuscriptAuthors * @param array $referAuthorships */ public function matchManuscriptToRefer(array $manuscriptAuthors, array $referAuthorships) { $manuscriptKeys = []; foreach ($manuscriptAuthors as $author) { if (($author['match_confidence'] ?? '') !== self::MATCH_PRECISE) { continue; } foreach ((array)($author['identity_keys'] ?? []) as $key) { $manuscriptKeys[$key] = $author; } } foreach ($referAuthorships as $auth) { foreach ((array)($auth['identity_keys'] ?? []) as $key) { if (isset($manuscriptKeys[$key])) { return $manuscriptKeys[$key]; } } } return null; } public function buildFuzzyAuthorKey($authorCitationPart) { $part = trim(preg_replace('/\.+$/u', '', trim((string)$authorCitationPart))); if ($part === '' || preg_match('/^et\s+al\.?$/iu', $part)) { return ''; } $tokens = preg_split('/\s+/u', $part, -1, PREG_SPLIT_NO_EMPTY); if (count($tokens) === 1) { return 'fuzzy:' . mb_strtoupper($tokens[0]) . '|'; } $last = array_pop($tokens); if (preg_match('/^[A-Za-z]{1,4}$/u', $last)) { $family = implode(' ', $tokens); return 'fuzzy:' . mb_strtoupper(preg_replace('/\s+/u', ' ', trim($family))) . '|' . mb_strtoupper($last); } $family = $last; $initials = ''; foreach ($tokens as $token) { $initials .= mb_strtoupper(mb_substr($token, 0, 1)); } return 'fuzzy:' . mb_strtoupper($family) . '|' . $initials; } public function extractFuzzyFirstAuthorKeyFromRefer(array $refer, array $meta) { $author = trim(trim((string)($meta['author'] ?? $refer['author'] ?? '')), '.'); if ($author === '') { return ''; } $parts = preg_split('/,\s*/u', $author); $first = trim((string)($parts[0] ?? '')); return $this->buildFuzzyAuthorKey($first); } private function resolveOneManuscriptAuthor(array $row) { $first = trim((string)($row['first_name'] ?? '')); $last = trim((string)($row['last_name'] ?? '')); $displayName = ($first !== '' && $last !== '') ? trim($first . ' ' . $last) : trim((string)($row['author_name'] ?? '')); $orcid = $this->bgCheck->cleanOrcid($row['orcid'] ?? ''); $openalexId = ''; $identityKeys = []; $confidence = self::MATCH_NONE; $fuzzyKey = ''; if ($last !== '') { $initials = $this->initialsFromGiven($first); $fuzzyKey = 'fuzzy:' . mb_strtoupper($last) . '|' . $initials; } if ($orcid !== '') { $identityKeys[] = 'orcid:' . $orcid; $confidence = self::MATCH_PRECISE; $res = $this->bgCheck->resolveAuthor(['orcid' => $orcid]); if (!empty($res['success']) && !empty($res['data'])) { $openalexId = $this->bgCheck->extractOpenAlexId($res['data']['id'] ?? ''); if ($openalexId !== '') { $identityKeys[] = 'openalex:' . $openalexId; } if (trim((string)($res['data']['display_name'] ?? '')) !== '') { $displayName = trim((string)$res['data']['display_name']); } } } return [ 'p_article_author_id' => intval($row['p_article_author_id']), 'display_name' => $displayName, 'openalex_id' => $openalexId, 'orcid' => $orcid, 'fuzzy_key' => $fuzzyKey, 'identity_keys' => array_values(array_unique($identityKeys)), 'match_confidence' => $confidence, ]; } private function initialsFromGiven($given) { $given = trim((string)$given); if ($given === '') { return ''; } $parts = preg_split('/[\s\-\.]+/u', $given, -1, PREG_SPLIT_NO_EMPTY); $initials = ''; foreach ($parts as $part) { $first = mb_substr($part, 0, 1); if ($first !== '') { $initials .= mb_strtoupper($first); } } return $initials; } private function fetchOpenAlexAuthorships($doi) { $res = $this->bgCheck->fetchOpenAlexWorkByDoi($doi); if (empty($res['success']) || empty($res['work']) || !is_array($res['work'])) { return []; } return $this->bgCheck->parseWorkAuthorships($res['work']); } private function fetchCrossrefAuthorships($doi) { $res = $this->bgCheck->fetchCrossRefWork($doi); if (empty($res['success']) || empty($res['message'])) { return []; } return $this->bgCheck->authorshipsFromCrossrefAuthors($res['message']['author'] ?? []); } private function pickFirstAuthorship(array $authorships) { if (empty($authorships)) { return null; } foreach ($authorships as $auth) { if (($auth['author_position'] ?? '') === 'first') { return $auth; } } return reset($authorships) ?: null; } }