Files
tougao/application/api/controller/Agent.php
2026-03-02 15:17:45 +08:00

414 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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,
]);
}
}