646 lines
23 KiB
PHP
646 lines
23 KiB
PHP
<?php
|
||
|
||
namespace app\common;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 参考文献作者明细入库/读取(供引用堆叠等同作者精准统计)
|
||
*/
|
||
class ReferenceReferAuthorService
|
||
{
|
||
/** @var BackgroundCheckService */
|
||
private $bgCheck;
|
||
|
||
/** @var CrossrefService */
|
||
private $crossref;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->bgCheck = new BackgroundCheckService();
|
||
$this->crossref = new CrossrefService([
|
||
'mailto' => trim((string)\think\Env::get('crossref_mailto', '')),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Crossref enrichment 成功后:解析并覆盖写入作者明细
|
||
*
|
||
* @return int 写入作者条数
|
||
*/
|
||
public function syncFromWorkSummary($pReferId, $pArticleId, $doi, array $summary)
|
||
{
|
||
$pReferId = intval($pReferId);
|
||
$pArticleId = intval($pArticleId);
|
||
if ($pReferId <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
$rows = $this->buildAuthorRows($doi, $summary);
|
||
Db::name('production_article_refer_author')->where('p_refer_id', $pReferId)->delete();
|
||
if (empty($rows)) {
|
||
return 0;
|
||
}
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
$insertRows = [];
|
||
foreach ($rows as $row) {
|
||
$insertRows[] = [
|
||
'p_article_id' => $pArticleId,
|
||
'p_refer_id' => $pReferId,
|
||
'author_seq' => intval($row['author_seq']),
|
||
'author_position' => $this->clipField((string)$row['author_position'], 16),
|
||
'is_first_author' => intval($row['is_first_author']),
|
||
'family' => $this->clipField((string)$row['family'], 128),
|
||
'given' => $this->clipField((string)$row['given'], 128),
|
||
'display_name' => $this->clipField((string)$row['display_name'], 256),
|
||
'citation_name' => $this->clipField((string)$row['citation_name'], 128),
|
||
'orcid' => $this->clipField((string)$row['orcid'], 32),
|
||
'openalex_id' => $this->clipField((string)$row['openalex_id'], 32),
|
||
'identity_source' => $this->clipField((string)$row['identity_source'], 32),
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
];
|
||
}
|
||
|
||
Db::name('production_article_refer_author')->insertAll($insertRows);
|
||
return count($insertRows);
|
||
}
|
||
|
||
/**
|
||
* 从 refer 表已有 author 字段解析并入库(Crossref/OpenAlex 均不可用时的兜底)
|
||
*
|
||
* @return int
|
||
*/
|
||
public function syncFromReferAuthorField($pReferId, $pArticleId, $authorString)
|
||
{
|
||
$pReferId = intval($pReferId);
|
||
$pArticleId = intval($pArticleId);
|
||
if ($pReferId <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
$rows = $this->parseReferAuthorString($authorString);
|
||
Db::name('production_article_refer_author')->where('p_refer_id', $pReferId)->delete();
|
||
if (empty($rows)) {
|
||
return 0;
|
||
}
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
$insertRows = [];
|
||
foreach ($rows as $row) {
|
||
$insertRows[] = [
|
||
'p_article_id' => $pArticleId,
|
||
'p_refer_id' => $pReferId,
|
||
'author_seq' => intval($row['author_seq']),
|
||
'author_position' => $this->clipField((string)$row['author_position'], 16),
|
||
'is_first_author' => intval($row['is_first_author']),
|
||
'family' => '',
|
||
'given' => '',
|
||
'display_name' => $this->clipField((string)$row['display_name'], 256),
|
||
'citation_name' => $this->clipField((string)$row['citation_name'], 128),
|
||
'orcid' => '',
|
||
'openalex_id' => '',
|
||
'identity_source' => 'refer_author',
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
];
|
||
}
|
||
|
||
Db::name('production_article_refer_author')->insertAll($insertRows);
|
||
return count($insertRows);
|
||
}
|
||
|
||
/**
|
||
* 单条参考文献同步作者明细:有 DOI 走 Crossref+OpenAlex,否则解析 refer.author
|
||
*
|
||
* @param int $pReferId
|
||
* @param int $pArticleId
|
||
* @param array $refer 可传 production_article_refer 行;为空则从库读取
|
||
* @return int 写入作者条数
|
||
*/
|
||
public function syncOneRefer($pReferId, $pArticleId, array $refer = [])
|
||
{
|
||
$pReferId = intval($pReferId);
|
||
$pArticleId = intval($pArticleId);
|
||
if ($pReferId <= 0 || $pArticleId <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
if (empty($refer)) {
|
||
$refer = Db::name('production_article_refer')
|
||
->where('p_refer_id', $pReferId)
|
||
->where('p_article_id', $pArticleId)
|
||
->where('state', 0)
|
||
->find();
|
||
if (empty($refer)) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
$refUtil = new ReferenceCheckService();
|
||
$doi = $refUtil->extractDoiFromRefer($refer);
|
||
$count = 0;
|
||
|
||
if ($doi !== '') {
|
||
$summary = $this->crossref->fetchWorkSummary($doi);
|
||
if ($summary === null || empty($summary['doi'])) {
|
||
$summary = ['doi' => $doi, 'raw' => []];
|
||
}
|
||
$count = $this->syncFromWorkSummary($pReferId, $pArticleId, $doi, $summary);
|
||
}
|
||
|
||
if ($count <= 0) {
|
||
$count = $this->syncFromReferAuthorField(
|
||
$pReferId,
|
||
$pArticleId,
|
||
(string)($refer['author'] ?? '')
|
||
);
|
||
}
|
||
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 按篇批量同步参考文献作者明细(有 DOI 则 Crossref + OpenAlex)
|
||
*
|
||
* @return array{total:int,synced:int,authors:int,skipped_no_doi:int,failed:int,errors:array}
|
||
*/
|
||
public function syncByPArticleId($pArticleId, array $options = [])
|
||
{
|
||
$pArticleId = intval($pArticleId);
|
||
if ($pArticleId <= 0) {
|
||
throw new \InvalidArgumentException('p_article_id is required');
|
||
}
|
||
|
||
$sleepMs = max(0, intval($options['sleep_ms'] ?? 100));
|
||
$refUtil = new ReferenceCheckService();
|
||
|
||
$refers = Db::name('production_article_refer')
|
||
->field('p_refer_id,p_article_id,index,refer_doi,doilink,refer_content,refer_frag,author')
|
||
->where('p_article_id', $pArticleId)
|
||
->where('state', 0)
|
||
->order('index asc')
|
||
->select();
|
||
|
||
$result = [
|
||
'p_article_id' => $pArticleId,
|
||
'total' => count($refers),
|
||
'synced' => 0,
|
||
'authors' => 0,
|
||
'skipped_no_doi' => 0,
|
||
'failed' => 0,
|
||
'errors' => [],
|
||
];
|
||
|
||
foreach ($refers as $refer) {
|
||
$pReferId = intval($refer['p_refer_id']);
|
||
$doi = $refUtil->extractDoiFromRefer($refer);
|
||
if ($doi === '') {
|
||
$result['skipped_no_doi']++;
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
$summary = $this->crossref->fetchWorkSummary($doi);
|
||
if ($summary === null || empty($summary['doi'])) {
|
||
$summary = ['doi' => $doi, 'raw' => []];
|
||
}
|
||
|
||
$count = $this->syncFromWorkSummary($pReferId, $pArticleId, $doi, $summary);
|
||
if ($count <= 0) {
|
||
$count = $this->syncFromReferAuthorField(
|
||
$pReferId,
|
||
$pArticleId,
|
||
(string)($refer['author'] ?? '')
|
||
);
|
||
}
|
||
|
||
if ($count > 0) {
|
||
$result['synced']++;
|
||
$result['authors'] += $count;
|
||
} else {
|
||
$result['failed']++;
|
||
$result['errors'][] = [
|
||
'p_refer_id' => $pReferId,
|
||
'reference_no' => intval($refer['index']) + 1,
|
||
'doi' => $doi,
|
||
'msg' => 'no author rows from Crossref/OpenAlex/refer.author',
|
||
];
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$result['failed']++;
|
||
$result['errors'][] = [
|
||
'p_refer_id' => $pReferId,
|
||
'reference_no' => intval($refer['index']) + 1,
|
||
'doi' => $doi,
|
||
'msg' => $e->getMessage(),
|
||
];
|
||
}
|
||
|
||
if ($sleepMs > 0) {
|
||
usleep($sleepMs * 1000);
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 单条参考文献:外网拉取作者并入库,再返回明细
|
||
* 有 DOI → Crossref + OpenAlex;失败或无 DOI → 解析 refer.author
|
||
*
|
||
* @return array{
|
||
* p_article_id:int,
|
||
* p_refer_id:int,
|
||
* reference_no:int,
|
||
* doi:string,
|
||
* synced:int,
|
||
* refer:array|null,
|
||
* author_count:int,
|
||
* authors:array
|
||
* }
|
||
*/
|
||
public function fetchAuthorsByPReferId($pArticleId, $pReferId)
|
||
{
|
||
$pArticleId = intval($pArticleId);
|
||
$pReferId = intval($pReferId);
|
||
if ($pArticleId <= 0 || $pReferId <= 0) {
|
||
throw new \InvalidArgumentException('p_article_id and p_refer_id are required');
|
||
}
|
||
|
||
$refer = Db::name('production_article_refer')
|
||
->where('p_refer_id', $pReferId)
|
||
->where('p_article_id', $pArticleId)
|
||
->where('state', 0)
|
||
->find();
|
||
|
||
if (empty($refer)) {
|
||
return [
|
||
'p_article_id' => $pArticleId,
|
||
'p_refer_id' => $pReferId,
|
||
'reference_no' => 0,
|
||
'doi' => '',
|
||
'synced' => 0,
|
||
'refer' => null,
|
||
'author_count' => 0,
|
||
'authors' => [],
|
||
];
|
||
}
|
||
|
||
$refUtil = new ReferenceCheckService();
|
||
$doi = $refUtil->extractDoiFromRefer($refer);
|
||
$syncedCount = $this->syncOneRefer($pReferId, $pArticleId, $refer);
|
||
$result = $this->getAuthorsByPReferId($pArticleId, $pReferId);
|
||
$result['doi'] = $doi;
|
||
$result['synced'] = $syncedCount > 0 ? 1 : 0;
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 按 p_article_id + p_refer_id 读取作者明细(只读本地库)
|
||
*
|
||
* @return array{p_article_id:int,p_refer_id:int,reference_no:int,refer:array|null,author_count:int,authors:array}
|
||
*/
|
||
public function getAuthorsByPReferId($pArticleId, $pReferId)
|
||
{
|
||
$pArticleId = intval($pArticleId);
|
||
$pReferId = intval($pReferId);
|
||
if ($pArticleId <= 0 || $pReferId <= 0) {
|
||
throw new \InvalidArgumentException('p_article_id and p_refer_id are required');
|
||
}
|
||
|
||
$refer = Db::name('production_article_refer')
|
||
->field('p_refer_id,p_article_id,index,author,title,joura,refer_doi,doilink,refer_type')
|
||
->where('p_refer_id', $pReferId)
|
||
->where('p_article_id', $pArticleId)
|
||
->where('state', 0)
|
||
->find();
|
||
|
||
if (empty($refer)) {
|
||
return [
|
||
'p_article_id' => $pArticleId,
|
||
'p_refer_id' => $pReferId,
|
||
'reference_no' => 0,
|
||
'refer' => null,
|
||
'author_count' => 0,
|
||
'authors' => [],
|
||
];
|
||
}
|
||
|
||
$rows = Db::name('production_article_refer_author')
|
||
->where('p_article_id', $pArticleId)
|
||
->where('p_refer_id', $pReferId)
|
||
->order('author_seq asc, id asc')
|
||
->field('author_seq,author_position,is_first_author,family,given,display_name,citation_name,orcid,openalex_id,identity_source')
|
||
->select();
|
||
|
||
$authors = [];
|
||
foreach ($rows as $row) {
|
||
$authors[] = [
|
||
'author_seq' => intval($row['author_seq'] ?? 0),
|
||
'author_position' => (string)($row['author_position'] ?? ''),
|
||
'is_first_author' => intval($row['is_first_author'] ?? 0),
|
||
'family' => (string)($row['family'] ?? ''),
|
||
'given' => (string)($row['given'] ?? ''),
|
||
'display_name' => (string)($row['display_name'] ?? ''),
|
||
'citation_name' => (string)($row['citation_name'] ?? ''),
|
||
'orcid' => (string)($row['orcid'] ?? ''),
|
||
'openalex_id' => (string)($row['openalex_id'] ?? ''),
|
||
'identity_source' => (string)($row['identity_source'] ?? ''),
|
||
];
|
||
}
|
||
|
||
return [
|
||
'p_article_id' => $pArticleId,
|
||
'p_refer_id' => $pReferId,
|
||
'reference_no' => intval($refer['index']) + 1,
|
||
'refer' => [
|
||
'p_refer_id' => $pReferId,
|
||
'reference_no' => intval($refer['index']) + 1,
|
||
'author' => (string)($refer['author'] ?? ''),
|
||
'title' => (string)($refer['title'] ?? ''),
|
||
'joura' => (string)($refer['joura'] ?? ''),
|
||
'refer_doi' => (string)($refer['refer_doi'] ?? ''),
|
||
'doilink' => (string)($refer['doilink'] ?? ''),
|
||
'refer_type' => (string)($refer['refer_type'] ?? ''),
|
||
],
|
||
'author_count' => count($authors),
|
||
'authors' => $authors,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 读取已入库的作者身份(与 ReferenceAuthorIdentityService 结构兼容)
|
||
*
|
||
* @return array<int, array{openalex_id:string,orcid:string,display_name:string,author_position:string,is_corresponding:bool,identity_keys:string[]}>
|
||
*/
|
||
public function loadAuthorshipsByPReferId($pReferId)
|
||
{
|
||
$pReferId = intval($pReferId);
|
||
if ($pReferId <= 0) {
|
||
return [];
|
||
}
|
||
|
||
$rows = Db::name('production_article_refer_author')
|
||
->where('p_refer_id', $pReferId)
|
||
->order('author_seq asc, id asc')
|
||
->select();
|
||
|
||
$list = [];
|
||
foreach ($rows as $row) {
|
||
$openalexId = trim((string)($row['openalex_id'] ?? ''));
|
||
$orcid = trim((string)($row['orcid'] ?? ''));
|
||
$identityKeys = [];
|
||
if ($openalexId !== '') {
|
||
$identityKeys[] = 'openalex:' . $openalexId;
|
||
}
|
||
if ($orcid !== '') {
|
||
$identityKeys[] = 'orcid:' . $orcid;
|
||
}
|
||
if (empty($identityKeys)) {
|
||
continue;
|
||
}
|
||
|
||
$position = trim((string)($row['author_position'] ?? ''));
|
||
if ($position === '' && intval($row['is_first_author'] ?? 0) === 1) {
|
||
$position = 'first';
|
||
}
|
||
|
||
$list[] = [
|
||
'openalex_id' => $openalexId,
|
||
'orcid' => $orcid,
|
||
'display_name' => trim((string)($row['display_name'] ?? '')),
|
||
'author_position' => $position,
|
||
'is_corresponding' => false,
|
||
'identity_keys' => $identityKeys,
|
||
];
|
||
}
|
||
|
||
return $list;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array>
|
||
*/
|
||
private function buildAuthorRows($doi, array $summary)
|
||
{
|
||
$doi = trim((string)$doi);
|
||
$raw = is_array($summary['raw'] ?? null) ? $summary['raw'] : [];
|
||
$crossrefAuthors = $this->parseCrossrefAuthorList($raw['author'] ?? []);
|
||
$openalexList = $doi !== '' ? $this->fetchOpenAlexAuthorships($doi) : [];
|
||
|
||
$max = max(count($crossrefAuthors), count($openalexList));
|
||
if ($max <= 0) {
|
||
return [];
|
||
}
|
||
|
||
$rows = [];
|
||
for ($i = 0; $i < $max; $i++) {
|
||
$cr = $crossrefAuthors[$i] ?? null;
|
||
$oa = $openalexList[$i] ?? null;
|
||
|
||
if (is_array($oa) && is_array($cr) && trim((string)($cr['orcid'] ?? '')) !== '' && trim((string)($oa['orcid'] ?? '')) === '') {
|
||
$oa['orcid'] = trim((string)$cr['orcid']);
|
||
}
|
||
if (is_array($oa) && is_array($cr) && trim((string)($oa['display_name'] ?? '')) === '' && trim((string)($cr['display_name'] ?? '')) !== '') {
|
||
$oa['display_name'] = trim((string)$cr['display_name']);
|
||
}
|
||
|
||
$row = $this->mergeOneAuthorRow($i, $cr, $oa);
|
||
if ($row !== null) {
|
||
$rows[] = $row;
|
||
}
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
private function mergeOneAuthorRow($seq, $crossrefAuthor, $openalexAuthor)
|
||
{
|
||
$family = '';
|
||
$given = '';
|
||
$displayName = '';
|
||
$citationName = '';
|
||
$orcid = '';
|
||
$openalexId = '';
|
||
$position = '';
|
||
$sources = [];
|
||
|
||
if (is_array($crossrefAuthor)) {
|
||
$family = trim((string)($crossrefAuthor['family'] ?? ''));
|
||
$given = trim((string)($crossrefAuthor['given'] ?? ''));
|
||
$displayName = trim((string)($crossrefAuthor['display_name'] ?? ''));
|
||
$citationName = trim((string)($crossrefAuthor['citation_name'] ?? ''));
|
||
$orcid = trim((string)($crossrefAuthor['orcid'] ?? ''));
|
||
$position = trim((string)($crossrefAuthor['author_position'] ?? ''));
|
||
$sources[] = 'crossref';
|
||
}
|
||
|
||
if (is_array($openalexAuthor)) {
|
||
$openalexId = trim((string)($openalexAuthor['openalex_id'] ?? ''));
|
||
if (trim((string)($openalexAuthor['orcid'] ?? '')) !== '') {
|
||
$orcid = trim((string)$openalexAuthor['orcid']);
|
||
}
|
||
if (trim((string)($openalexAuthor['display_name'] ?? '')) !== '') {
|
||
$displayName = trim((string)$openalexAuthor['display_name']);
|
||
}
|
||
if (trim((string)($openalexAuthor['author_position'] ?? '')) !== '') {
|
||
$position = trim((string)$openalexAuthor['author_position']);
|
||
}
|
||
$sources[] = 'openalex';
|
||
}
|
||
|
||
if ($displayName === '' && ($family !== '' || $given !== '')) {
|
||
$displayName = trim($given . ' ' . $family);
|
||
}
|
||
if ($citationName === '' && is_array($crossrefAuthor) && !empty($crossrefAuthor['raw_author'])) {
|
||
$citationName = $this->crossref->getAuthorsCitation(['author' => [$crossrefAuthor['raw_author']]], 1);
|
||
}
|
||
if ($citationName === '' && $displayName !== '') {
|
||
$citationName = $displayName;
|
||
}
|
||
|
||
if ($displayName === '' && $citationName === '' && $orcid === '' && $openalexId === '') {
|
||
return null;
|
||
}
|
||
|
||
$sources = array_values(array_unique($sources));
|
||
$isFirst = ($position === 'first') || ($seq === 0 && $position === '');
|
||
|
||
return [
|
||
'author_seq' => intval($seq),
|
||
'author_position' => $position,
|
||
'is_first_author' => $isFirst ? 1 : 0,
|
||
'family' => $family,
|
||
'given' => $given,
|
||
'display_name' => $displayName,
|
||
'citation_name' => $citationName,
|
||
'orcid' => $orcid,
|
||
'openalex_id' => $openalexId,
|
||
'identity_source' => implode('+', $sources),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array>
|
||
*/
|
||
private function parseCrossrefAuthorList($authorList)
|
||
{
|
||
if (empty($authorList) || !is_array($authorList)) {
|
||
return [];
|
||
}
|
||
|
||
$parsed = $this->bgCheck->parseCrossRefAuthors($authorList);
|
||
$rows = [];
|
||
foreach ($parsed as $i => $item) {
|
||
$rawAuthor = $authorList[$i] ?? [];
|
||
if (!is_array($rawAuthor)) {
|
||
$rawAuthor = [];
|
||
}
|
||
$family = trim((string)($item['family'] ?? ''));
|
||
$given = trim((string)($item['given'] ?? ''));
|
||
$displayName = trim((string)($item['name'] ?? ''));
|
||
$position = trim((string)($rawAuthor['sequence'] ?? ''));
|
||
if ($position === '' && $i === 0) {
|
||
$position = 'first';
|
||
}
|
||
|
||
$rows[] = [
|
||
'family' => $family,
|
||
'given' => $given,
|
||
'display_name' => $displayName,
|
||
'orcid' => trim((string)($item['orcid'] ?? '')),
|
||
'author_position' => $position,
|
||
'citation_name' => $this->crossref->getAuthorsCitation(['author' => [$rawAuthor]], 1),
|
||
'raw_author' => $rawAuthor,
|
||
];
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array{openalex_id:string,orcid:string,display_name:string,author_position:string}>
|
||
*/
|
||
private function fetchOpenAlexAuthorships($doi)
|
||
{
|
||
$res = $this->bgCheck->fetchOpenAlexWorkByDoi($doi);
|
||
if (empty($res['success']) || empty($res['work']) || !is_array($res['work'])) {
|
||
return [];
|
||
}
|
||
|
||
$list = [];
|
||
foreach ($res['work']['authorships'] ?? [] as $auth) {
|
||
if (!is_array($auth)) {
|
||
continue;
|
||
}
|
||
$author = is_array($auth['author'] ?? null) ? $auth['author'] : [];
|
||
$openalexId = $this->bgCheck->extractOpenAlexId($author['id'] ?? '');
|
||
$orcid = $this->bgCheck->cleanOrcid($author['orcid'] ?? '');
|
||
$displayName = trim((string)($author['display_name'] ?? ''));
|
||
if ($openalexId === '' && $orcid === '' && $displayName === '') {
|
||
continue;
|
||
}
|
||
|
||
$list[] = [
|
||
'openalex_id' => $openalexId,
|
||
'orcid' => $orcid,
|
||
'display_name' => $displayName,
|
||
'author_position' => (string)($auth['author_position'] ?? ''),
|
||
'is_corresponding' => !empty($auth['is_corresponding']),
|
||
];
|
||
}
|
||
|
||
return $list;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array{author_seq:int,author_position:string,is_first_author:int,display_name:string,citation_name:string}>
|
||
*/
|
||
private function parseReferAuthorString($authorString)
|
||
{
|
||
$authorString = trim((string)$authorString);
|
||
if ($authorString === '') {
|
||
return [];
|
||
}
|
||
|
||
$authorString = preg_replace('/\s+et\s+al\.?\s*$/iu', '', $authorString);
|
||
$parts = preg_split('/\s*,\s*/u', $authorString);
|
||
if (!is_array($parts)) {
|
||
return [];
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($parts as $i => $part) {
|
||
$name = trim((string)$part);
|
||
if ($name === '' || preg_match('/^et\s+al\.?$/iu', $name)) {
|
||
continue;
|
||
}
|
||
$seq = count($rows);
|
||
$rows[] = [
|
||
'author_seq' => $seq,
|
||
'author_position' => $seq === 0 ? 'first' : 'additional',
|
||
'is_first_author' => $seq === 0 ? 1 : 0,
|
||
'display_name' => $name,
|
||
'citation_name' => $name,
|
||
];
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
private function clipField($value, $maxLen)
|
||
{
|
||
$value = trim((string)$value);
|
||
if ($value === '' || $maxLen <= 0) {
|
||
return $value;
|
||
}
|
||
if (mb_strlen($value) <= $maxLen) {
|
||
return $value;
|
||
}
|
||
return mb_substr($value, 0, $maxLen);
|
||
}
|
||
}
|