From 413839f87b4ffdded0e01f04bb651c605c2ccd8b Mon Sep 17 00:00:00 2001 From: wangjinlei <751475802@qq.com> Date: Mon, 2 Mar 2026 15:17:45 +0800 Subject: [PATCH] major agent controller --- application/api/controller/Agent.php | 413 +++++++++++++++++++++++++ application/api/controller/Article.php | 2 +- 2 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 application/api/controller/Agent.php diff --git a/application/api/controller/Agent.php b/application/api/controller/Agent.php new file mode 100644 index 0000000..768ed56 --- /dev/null +++ b/application/api/controller/Agent.php @@ -0,0 +1,413 @@ +major_obj + ->where('major_state', 0) + ->where('major_type', 0) + ->select(); + + $majorMap = []; + foreach ($allMajors as $m) { + $majorMap[$m['major_id']] = $m; + } + + $result = []; + foreach ($allMajors as $m) { + $hasChild = false; + foreach ($allMajors as $check) { + if ($check['pid'] == $m['major_id']) { + $hasChild = true; + break; + } + } + if (!$hasChild) { + $path = $this->buildMajorPath($m['major_id'], $majorMap); + $result[] = [ + 'major_id' => $m['major_id'], + 'major_title' => $m['major_title'], + 'full_path' => $path, + ]; + } + } + + Cache::set($cacheKey, $result, 3600); + return $result; + } + + /** + * 递归构建 major 的完整路径 + */ + private function buildMajorPath($majorId, &$majorMap) + { + if (!isset($majorMap[$majorId])) { + return ''; + } + $m = $majorMap[$majorId]; + if ($m['pid'] == 0 || $m['pid'] == 1 || !isset($majorMap[$m['pid']])) { + return $m['major_title']; + } + return $this->buildMajorPath($m['pid'], $majorMap) . ' > ' . $m['major_title']; + } + + /** + * 构建 major 列表提示文本(供 AI 使用) + */ + private function buildMajorListPrompt($majorTree) + { + $lines = []; + foreach ($majorTree as $item) { + $lines[] = "ID:{$item['major_id']} - {$item['full_path']}"; + } + return implode("\n", $lines); + } + + /** + * 调用 AI 将用户 field 描述匹配到标准 major_id + */ + private function matchFieldToMajor($field, $majorListPrompt) + { + $systemPrompt = "你是一位医学领域分类专家。用户会提供一段研究领域的描述文本,你需要从给定的标准领域列表中找出最匹配的1-3个领域。\n" + . "请严格按照JSON数组格式返回匹配结果,只返回major_id数组,如 [12,34,56]。\n" + . "如果没有合适的匹配,返回空数组 []。\n" + . "不要返回任何其他内容,只返回JSON数组。\n\n" + . "标准领域列表:\n" . $majorListPrompt; + + $userPrompt = "请为以下研究领域描述匹配最合适的标准领域ID:\n" . $field; + + $messages = [ + ['role' => 'system', 'content' => $systemPrompt], + ['role' => 'user', 'content' => $userPrompt], + ]; + + $apiKey = Env::get("gpt.api_key1", Env::get("gpt.api_key", "")); + $url = 'http://chat.taimed.cn/v1/chat/completions'; + + $data = [ + 'model' => 'gpt-4.1', + 'messages' => $messages, + 'temperature' => 0.1, + 'max_tokens' => 200, + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $apiKey, + ]); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); + + $result = curl_exec($ch); + if (curl_errno($ch)) { + curl_close($ch); + return []; + } + curl_close($ch); + + $res = json_decode($result, true); + if (!isset($res['choices'][0]['message']['content'])) { + return []; + } + + $content = trim($res['choices'][0]['message']['content']); + // 提取 JSON 数组 + if (preg_match('/\[[\d,\s]*\]/', $content, $matches)) { + $ids = json_decode($matches[0], true); + if (is_array($ids)) { + return array_map('intval', $ids); + } + } + + return []; + } + + /** + * 将匹配结果写入 t_major_to_user + */ + private function saveMajorToUser($userId, $majorIds) + { + $existing = $this->major_to_user_obj + ->where('user_id', $userId) + ->where('state', 0) + ->column('major_id'); + + $toInsert = array_diff($majorIds, $existing); + foreach ($toInsert as $majorId) { + $this->major_to_user_obj->insert([ + 'user_id' => $userId, + 'major_id' => $majorId, + 'ctime' => time(), + ]); + } + + return count($toInsert); + } + + /** + * 处理单个用户的 field 转 major + * + * @param int user_id 用户ID + */ + public function processOneUser() + { + $data = $this->request->param(); + if (!isset($data['user_id']) || $data['user_id'] == '') { + return jsonError('user_id不能为空'); + } + + $userId = intval($data['user_id']); + $reviewerInfo = $this->user_reviewer_info_obj + ->where('reviewer_id', $userId) + ->where('state', 0) + ->find(); + + if (!$reviewerInfo) { + return jsonError('未找到该用户的reviewer信息'); + } + + $field = trim($reviewerInfo['field']); + if ($field == '') { + return jsonError('该用户的field字段为空'); + } + + $majorTree = $this->getMajorTree(); + if (empty($majorTree)) { + return jsonError('未获取到标准领域数据'); + } + + $majorListPrompt = $this->buildMajorListPrompt($majorTree); + + + + $matchedIds = $this->matchFieldToMajor($field, $majorListPrompt); + + if (empty($matchedIds)) { + return jsonSuccess([ + 'user_id' => $userId, + 'field' => $field, + 'matched_ids' => [], + 'inserted' => 0, + 'msg' => 'AI未匹配到合适的领域', + ]); + } + + // 验证 major_id 确实存在 + $validMajors = $this->major_obj + ->where('major_id', 'in', $matchedIds) + ->where('major_state', 0) + ->select(); +// ->column('major_id'); +// $matchedIds = array_intersect($matchedIds, $validMajors); + +// $inserted = $this->saveMajorToUser($userId, $matchedIds); + + foreach ($validMajors as $k => $major){ + $validMajors[$k]['shu'] = getMajorShu($major['major_id']); + } + + + + return jsonSuccess([ + 'user_id' => $userId, + 'field' => $field, + 'majors' => $validMajors, +// 'inserted' => $inserted, + ]); + } + + /** + * 批量处理:获取有 field 但没有 major_to_user 记录的用户,逐个用 AI 匹配 + * + * @param int limit 每次处理的数量,默认10 + * @param int skip_has_major 是否跳过已有major_to_user记录的用户,默认1 + */ + public function batchProcess() + { + $data = $this->request->param(); + $limit = isset($data['limit']) ? intval($data['limit']) : 10; + $skipHasMajor = isset($data['skip_has_major']) ? intval($data['skip_has_major']) : 1; + + if ($limit > 50) { + $limit = 50; + } + + $query = $this->user_reviewer_info_obj + ->alias('ri') + ->field('ri.reviewer_id, ri.field') + ->where('ri.state', 0) + ->where('ri.field', '<>', ''); + + if ($skipHasMajor) { + $subQuery = Db::name('major_to_user')->where('state', 0)->field('user_id')->buildSql(); + $query = $query->where('ri.reviewer_id', 'not in', $subQuery); + } + + $users = $query->limit($limit)->select(); + + if (empty($users)) { + return jsonSuccess([ + 'processed' => 0, + 'msg' => '没有需要处理的用户', + ]); + } + + $majorTree = $this->getMajorTree(); + if (empty($majorTree)) { + return jsonError('未获取到标准领域数据'); + } + $majorListPrompt = $this->buildMajorListPrompt($majorTree); + + $validMajorIds = $this->major_obj->where('major_state', 0)->column('major_id'); + + $results = []; + $successCount = 0; + $failCount = 0; + + foreach ($users as $user) { + $field = trim($user['field']); + if ($field == '') { + continue; + } + + $matchedIds = $this->matchFieldToMajor($field, $majorListPrompt); + $matchedIds = array_intersect($matchedIds, $validMajorIds); + + if (!empty($matchedIds)) { + $inserted = $this->saveMajorToUser($user['reviewer_id'], $matchedIds); + $results[] = [ + 'user_id' => $user['reviewer_id'], + 'field' => mb_substr($field, 0, 100), + 'matched_ids' => array_values($matchedIds), + 'inserted' => $inserted, + ]; + $successCount++; + } else { + $results[] = [ + 'user_id' => $user['reviewer_id'], + 'field' => mb_substr($field, 0, 100), + 'matched_ids' => [], + 'inserted' => 0, + ]; + $failCount++; + } + } + + return jsonSuccess([ + 'processed' => count($results), + 'success_count' => $successCount, + 'fail_count' => $failCount, + 'details' => $results, + ]); + } + + /** + * 查看当前 major 树结构(调试用) + */ + public function getMajorList() + { + $majorTree = $this->getMajorTree(); + return jsonSuccess([ + 'total' => count($majorTree), + 'list' => $majorTree, + ]); + } + + /** + * 从 Excel 文件导入 major 数据到数据库(如需要) + */ + public function importMajorFromExcel() + { + $file = ROOT_PATH . 'public' . DS . 'system' . DS . 't_major.xlsx'; + if (!file_exists($file)) { + return jsonError('Excel文件不存在: public/system/t_major.xlsx'); + } + + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file); + $sheet = $spreadsheet->getActiveSheet(); + $highestRow = $sheet->getHighestRow(); + $highestColumn = $sheet->getHighestColumn(); + + $headers = []; + $colCount = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); + for ($col = 1; $col <= $colCount; $col++) { + $headers[$col] = $sheet->getCellByColumnAndRow($col, 1)->getValue(); + } + + $rows = []; + for ($row = 2; $row <= $highestRow; $row++) { + $rowData = []; + for ($col = 1; $col <= $colCount; $col++) { + $rowData[$headers[$col]] = $sheet->getCellByColumnAndRow($col, $row)->getValue(); + } + $rows[] = $rowData; + } + + return jsonSuccess([ + 'headers' => array_values($headers), + 'total' => count($rows), + 'preview' => array_slice($rows, 0, 20), + ]); + } + + /** + * 统计当前 field 转 major 的覆盖情况 + */ + public function statistics() + { + $totalReviewers = $this->user_reviewer_info_obj + ->where('state', 0) + ->count(); + + $hasField = $this->user_reviewer_info_obj + ->where('state', 0) + ->where('field', '<>', '') + ->count(); + + $hasMajorToUser = Db::name('major_to_user') + ->where('state', 0) + ->group('user_id') + ->count(); + + $hasFieldNoMajor = $this->user_reviewer_info_obj + ->alias('ri') + ->where('ri.state', 0) + ->where('ri.field', '<>', '') + ->where('ri.reviewer_id', 'not in', Db::name('major_to_user')->where('state', 0)->field('user_id')->buildSql()) + ->count(); + + return jsonSuccess([ + 'total_reviewers' => $totalReviewers, + 'has_field' => $hasField, + 'has_major_to_user' => $hasMajorToUser, + 'has_field_no_major' => $hasFieldNoMajor, + ]); + } +} diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 28a7be9..8ab5d3b 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -743,7 +743,7 @@ class Article extends Base $journal_info = $this->journal_obj->where('journal_id', $article_old_info['journal_id'])->find(); $editor_info = $this->user_obj->where('user_id', $journal_info['editor_id'])->find(); $tt = 'Dear editor,
'; - $tt .= 'The author changed the manuscript’s status, please check.

'; + $tt .= 'The author changed the manuscript’s status, please check. sn:' . $article_old_info['accept_sn'] . '

'; $tt .= 'TMR Publishing Group'; // $sendUser=[