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=[