major agent controller
This commit is contained in:
413
application/api/controller/Agent.php
Normal file
413
application/api/controller/Agent.php
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
use think\Cache;
|
||||||
|
use think\Env;
|
||||||
|
|
||||||
|
class Agent extends Base
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct(\think\Request $request = null)
|
||||||
|
{
|
||||||
|
parent::__construct($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有叶子节点 major 及其完整路径,用于 AI 匹配
|
||||||
|
*/
|
||||||
|
private function getMajorTree()
|
||||||
|
{
|
||||||
|
$cacheKey = 'agent_major_tree';
|
||||||
|
$cached = Cache::get($cacheKey);
|
||||||
|
if ($cached) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allMajors = $this->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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -743,7 +743,7 @@ class Article extends Base
|
|||||||
$journal_info = $this->journal_obj->where('journal_id', $article_old_info['journal_id'])->find();
|
$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();
|
$editor_info = $this->user_obj->where('user_id', $journal_info['editor_id'])->find();
|
||||||
$tt = 'Dear editor,<br>';
|
$tt = 'Dear editor,<br>';
|
||||||
$tt .= 'The author changed the manuscript’s status, please check.<br><br>';
|
$tt .= 'The author changed the manuscript’s status, please check. sn:' . $article_old_info['accept_sn'] . '<br><br>';
|
||||||
$tt .= 'TMR Publishing Group';
|
$tt .= 'TMR Publishing Group';
|
||||||
|
|
||||||
// $sendUser=[
|
// $sendUser=[
|
||||||
|
|||||||
Reference in New Issue
Block a user