From 1d54946fef97376f7c2789af83a1616dd6f7a380 Mon Sep 17 00:00:00 2001 From: wyn <1074145239@qq.com> Date: Fri, 5 Jun 2026 13:52:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E8=83=8C=E8=B0=83=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?=E5=8F=91=E9=82=AE=E4=BB=B6=E8=AE=B0=E5=BD=95=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A7=93=E5=90=8D=E5=92=8C=E9=82=AE=E7=AE=B1=E6=A8=A1=E7=B3=8A?= =?UTF-8?q?=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Author.php | 157 +++++++++++++++++- application/api/controller/EmailClient.php | 5 +- application/api/view/author/_styles.html | 51 ++++++ application/api/view/author/index.html | 8 +- application/api/view/author/report.html | 26 +++ .../service/AuthorBackgroundService.php | 7 +- 6 files changed, 243 insertions(+), 11 deletions(-) diff --git a/application/api/controller/Author.php b/application/api/controller/Author.php index 99b22daa..f024a1f2 100644 --- a/application/api/controller/Author.php +++ b/application/api/controller/Author.php @@ -4,6 +4,7 @@ namespace app\api\controller; use app\common\service\AuthorBackgroundService; use think\Controller; +use think\Db; /** * 作者背调:HTML 报告页 + JSON API @@ -15,6 +16,9 @@ class Author extends Controller /** @var AuthorBackgroundService */ private $bgService; + /** @var string */ + private $articleLookupError = ''; + public function __construct(\think\Request $request = null) { parent::__construct($request); @@ -25,7 +29,8 @@ class Author extends Controller * 作者背调 HTML 页面入口 * * 1. 传了 ORCID → 直接生成报告 - * 2. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID;1 条直接报告,多条显示选择列表 + * 2. 传了 articleId(稿件作者 ID,即 art_aut_id)→ 从 t_article_author 补全 ORCID / 姓名 / 机构 + * 3. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID;1 条直接报告,多条显示选择列表 */ public function index() { @@ -33,6 +38,18 @@ class Author extends Controller $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 === '' @@ -129,6 +146,8 @@ class Author extends Controller */ private function resolveBackgroundParams() { + $this->articleLookupError = ''; + $pick = function (...$keys) { foreach ($keys as $k) { $v = trim((string) input('param.' . $k, '')); @@ -145,14 +164,142 @@ class Author extends Controller 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' => $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'), + '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'; diff --git a/application/api/controller/EmailClient.php b/application/api/controller/EmailClient.php index 63841fc0..8fda8ff3 100644 --- a/application/api/controller/EmailClient.php +++ b/application/api/controller/EmailClient.php @@ -2130,6 +2130,7 @@ class EmailClient extends Base $state = $this->request->param('state', '-1'); $page = max(1, intval($this->request->param('page', 1))); $perPage = max(1, min(intval($this->request->param('per_page', 50)), 200)); + $keyword = trim($this->request->param('keyword', '')); if (!$taskId) { return jsonError('task_id is required'); @@ -2139,7 +2140,9 @@ class EmailClient extends Base if ($state !== '-1' && $state !== '') { $where['l.state'] = intval($state); } - + if ($keyword !== '') { + $where['e.email|e.name'] = ['like',"%".trim($keyword)."%"]; + } $total = Db::name('promotion_email_log')->alias('l')->where($where)->count(); $list = Db::name('promotion_email_log')->alias('l') ->join('t_expert e', 'l.expert_id = e.expert_id', 'LEFT') diff --git a/application/api/view/author/_styles.html b/application/api/view/author/_styles.html index d84863ec..19d52582 100644 --- a/application/api/view/author/_styles.html +++ b/application/api/view/author/_styles.html @@ -289,6 +289,57 @@ a.ext:hover { text-decoration: underline; } margin: 6px 0; } +.report-rules { + margin-top: 32px; + padding: 18px 22px; + background: #f8fafc; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 13px; + color: var(--text-muted); + line-height: 1.7; +} +.report-rules-title { + font-size: 14px; + font-weight: 700; + color: var(--text); + margin: 0 0 12px; +} +.report-rules-sub { + font-size: 13px; + font-weight: 600; + color: var(--text); + margin: 14px 0 8px; +} +.rules-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.rules-table th, +.rules-table td { + border: 1px solid var(--border); + padding: 8px 10px; + text-align: left; + vertical-align: top; +} +.rules-table th { + background: #edf2f7; + color: var(--text); + font-weight: 600; + width: 22%; +} +.rules-table td strong { + color: var(--text); +} +.report-rules ul { + margin: 0; + padding-left: 18px; +} +.report-rules li { + margin: 4px 0; +} + .report-foot { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-muted); text-align: center; line-height: 1.7; diff --git a/application/api/view/author/index.html b/application/api/view/author/index.html index 380184e7..bd60a5ef 100644 --- a/application/api/view/author/index.html +++ b/application/api/view/author/index.html @@ -26,19 +26,19 @@
- +
- +
- +
- +
diff --git a/application/api/view/author/report.html b/application/api/view/author/report.html index b67033f3..c9205318 100644 --- a/application/api/view/author/report.html +++ b/application/api/view/author/report.html @@ -239,6 +239,32 @@ {/notempty} +
+

学术指标说明

+ + + + + + + +
论文总数OpenAlex、ORCID、PubMed、Scopus 四项中的最大值(非相加)。OpenAlex 无档案时,可由 ORCID / PubMed 等补全。
总被引仅来自 OpenAlex;未匹配到作者档案时为 0。
H 指数仅来自 OpenAlex;未匹配时为 0。
i10 指数仅来自 OpenAlex(至少 10 次被引的论文数);未匹配时为 0。
PubMed独立统计,与论文总数口径不同。检索顺序:ORCID → 姓名+机构 → 姓名,采用第一个有结果的检索式;列表最多显示最近 10 篇。
研究方向来自 OpenAlex 前 5 个主题;无 OpenAlex 档案时不显示。
+

风险评级

+ +

其他说明

+ +
+

数据来源:OpenAlex · ORCID · PubMed · Scopus · Retraction Watch
适用于青年编委 / 特约审稿人 / 作者资质初审 diff --git a/application/common/service/AuthorBackgroundService.php b/application/common/service/AuthorBackgroundService.php index 469dbcda..0541d287 100644 --- a/application/common/service/AuthorBackgroundService.php +++ b/application/common/service/AuthorBackgroundService.php @@ -989,9 +989,14 @@ class AuthorBackgroundService } $papers = $this->pubmedFetchSummaries($ids); } + $urlTerm = $usedTerm; + if (preg_match('/^(.+)\[ORCID\]$/i', $usedTerm, $m)) { + $urlTerm = $m[1]; + } + return [ 'total' => $total, 'papers' => $papers, 'query' => $usedTerm, - 'pubmed_url' => 'https://pubmed.ncbi.nlm.nih.gov/?term=' . urlencode($usedTerm), + 'pubmed_url' => 'https://pubmed.ncbi.nlm.nih.gov/?term=' . urlencode($urlTerm), ]; } From aee9b00c6f13e7bcdd17797152a990ef9fab5116 Mon Sep 17 00:00:00 2001 From: wyn <1074145239@qq.com> Date: Mon, 8 Jun 2026 11:00:54 +0800 Subject: [PATCH 2/2] =?UTF-8?q?api/expert=5Fmanage/getList=20=E5=8A=A0?= =?UTF-8?q?=E5=9B=BD=E5=AE=B6=E7=AD=9B=E9=80=89=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E7=AE=80=E5=8E=86=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Author.php | 3 +- application/api/controller/EmailClient.php | 7 ++- application/api/controller/ExpertManage.php | 5 ++ application/api/controller/User.php | 59 +++++++++++++++++++++ application/common/service/LLMService.php | 29 ++++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/application/api/controller/Author.php b/application/api/controller/Author.php index f024a1f2..fa55016e 100644 --- a/application/api/controller/Author.php +++ b/application/api/controller/Author.php @@ -302,7 +302,8 @@ class Author extends Controller private function resolveFormAction() { - return rtrim($this->request->root(), '/') . '/api/author/index'; + // 生产环境未配置伪静态时需带 index.php,如 /public/index.php/api/author/index + return rtrim($this->request->baseFile(), '/') . '/api/author/index'; } private function renderReportPage(array $params, $formAction) diff --git a/application/api/controller/EmailClient.php b/application/api/controller/EmailClient.php index 8fda8ff3..b243f43b 100644 --- a/application/api/controller/EmailClient.php +++ b/application/api/controller/EmailClient.php @@ -2141,9 +2141,12 @@ class EmailClient extends Base $where['l.state'] = intval($state); } if ($keyword !== '') { - $where['e.email|e.name'] = ['like',"%".trim($keyword)."%"]; + $where['e.email|e.name'] = ['like', '%' . $keyword . '%']; } - $total = Db::name('promotion_email_log')->alias('l')->where($where)->count(); + $total = Db::name('promotion_email_log')->alias('l') + ->join('t_expert e', 'l.expert_id = e.expert_id', 'LEFT') + ->where($where) + ->count('l.log_id'); $list = Db::name('promotion_email_log')->alias('l') ->join('t_expert e', 'l.expert_id = e.expert_id', 'LEFT') ->where($where) diff --git a/application/api/controller/ExpertManage.php b/application/api/controller/ExpertManage.php index bd3a0fea..b8c473d9 100644 --- a/application/api/controller/ExpertManage.php +++ b/application/api/controller/ExpertManage.php @@ -39,6 +39,7 @@ class ExpertManage extends Base $field = trim(isset($data['field']) ? $data['field'] : ''); $state = isset($data['state']) ? $data['state'] : '-1'; $source = trim(isset($data['source']) ? $data['source'] : ''); + $country = trim(isset($data['country']) ? $data['country'] : ''); $page = max(1, intval(isset($data['pageIndex']) ? $data['pageIndex'] : 1)); $pageSize = max(1, intval(isset($data['pageSize']) ? $data['pageSize'] : 20)); @@ -74,6 +75,10 @@ class ExpertManage extends Base $query->where('e.source', $source); $countQuery->where('e.source', $source); } + if ($country !== '') { + $query->where('e.country', $country); + $countQuery->where('e.country', $country); + } // $countQuery = clone $query; // $total = $countQuery->distinct('e.expert_id')->count(); diff --git a/application/api/controller/User.php b/application/api/controller/User.php index 27a9a750..1b8edef4 100644 --- a/application/api/controller/User.php +++ b/application/api/controller/User.php @@ -3311,4 +3311,63 @@ class User extends Base return jsonSuccess([]); } + /** + * 根据 user_id 查 user_cv 简历,调用大模型解析用户基本信息。 + * + * POST/GET user_id=用户ID(也支持 userId) + * 简历地址:https://submission.tmrjournals.com/public/reviewer/{cv} + * 同机部署时优先读 public/reviewer/ 本地文件。 + */ + public function getUserInfoByFile() + { + @set_time_limit(180); + + $userId = intval($this->request->param('user_id', $this->request->param('userId', 0))); + if ($userId <= 0) { + return jsonError('请提供 user_id'); + } + + try { + $service = new \app\common\UserInfoFromFileService(); + $result = $service->parseFromUserId($userId); + + \think\Log::info('[getUserInfoByFile] user_id=' . $userId . ' ' . json_encode($result['user_info'], JSON_UNESCAPED_UNICODE)); + + return jsonSuccess([ + 'message' => '解析完成', + 'user_id' => $result['user_id'], + 'cv' => $result['cv'], + 'cv_url' => $result['cv_url'], + 'file' => $result['file'], + 'text_length' => $result['text_length'], + 'text_preview' => $result['text_preview'], + 'user_info' => $result['user_info'], + 'print' => $this->formatUserInfoForPrint($result['user_info']), + ]); + } catch (\Throwable $e) { + return jsonError($e->getMessage()); + } + } + + private function formatUserInfoForPrint(array $info) + { + $labels = [ + 'realname' => '姓名', + 'email' => '邮箱', + 'phone' => '电话', + 'orcid' => 'ORCID', + 'technical' => '职称', + 'field' => '研究领域', + 'company' => 'Institution', + 'department' => '科室', + 'country' => '国家', + 'introduction' => '简介', + ]; + $lines = []; + foreach ($labels as $key => $label) { + $val = trim((string) ($info[$key] ?? '')); + $lines[] = $label . ':' . ($val !== '' ? $val : '(未识别)'); + } + return implode("\n", $lines); + } } diff --git a/application/common/service/LLMService.php b/application/common/service/LLMService.php index fdd1fb42..20e25fc1 100644 --- a/application/common/service/LLMService.php +++ b/application/common/service/LLMService.php @@ -120,6 +120,35 @@ class LLMService ]; } + /** + * 通用对话请求(与参考文献校对共用 postChat / [promotion] 配置) + * + * @param array $messages OpenAI messages + * @param float $temperature + * @return string|null 助手回复正文 + */ + public function requestChat(array $messages, $temperature = 0) + { + if ($this->url === '' || $this->model === '') { + \think\Log::warning('LLM requestChat: url or model not configured'); + return null; + } + $payload = [ + 'model' => $this->model, + 'temperature' => $temperature, + 'messages' => $messages, + ]; + return $this->postChat($payload); + } + + /** + * 解析模型返回的 JSON 对象(去除 markdown 代码块等) + */ + public function parseJsonResponse($raw) + { + return $this->parseJson($raw); + } + /** * 解析 can_support;兼容 is_match 字段 */