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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user