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, ]); } }