0.6, // 技能匹配权重 'composite' => 0.2, // 综合评分权重 'experience' => 0.1, // 经验权重 'reputation' => 0.1 // 信誉权重 ]; private $stopWords = []; private $jieba; /** * 构造函数 * @param array $config 配置参数 */ public function __construct($config = []) { // 初始化权重配置 if (isset($config['weights'])) { $this->weights = array_merge($this->weights, $config['weights']); } // 加载停用词表 $this->loadStopWords(); // 初始化分词工具(需要安装 jieba-php 扩展) // $this->jieba = new \Fukuball\Jieba\Jieba(); // $this->jieba->init(); } /** * 主匹配方法 * @param array $demand 需求数据 * @param array $programmers 程序员列表 * @param int $topN 返回前N名 * @return array 匹配结果 */ public function match($demand, $programmers, $topN = 5) { // 1. 需求分析 $demandProfile = $this->analyzeDemand($demand); // 2. 为每个程序员计算匹配度 $matches = []; foreach ($programmers as $programmer) { // 构建程序员画像 $programmerProfile = $this->buildProgrammerProfile($programmer); // 计算匹配度 $matchScore = $this->calculateMatchScore($demandProfile, $programmerProfile); $matches[] = [ 'programmer' => $programmer, 'score' => $matchScore, 'details' => $this->getMatchDetails($demandProfile, $programmerProfile) ]; } // 3. 按匹配度排序并返回前N名 usort($matches, function($a, $b) { return $b['score'] <=> $a['score']; }); return array_slice($matches, 0, $topN); } /** * 需求分析 * @param array $demand 需求数据 * @return array 需求画像 */ private function analyzeDemand($demand) { $keywords = []; // 组合标题和描述进行分词 $text = $demand['title'] . ' ' . ($demand['description'] ?? ''); // 使用分词工具(这里用简单空格分词模拟,实际使用jieba) // $words = $this->jieba->cut($text); $words = $this->simpleChineseSegment($text); // 去停用词和统计词频 $wordFreq = []; foreach ($words as $word) { $word = trim($word); if (empty($word) || in_array($word, $this->stopWords) || mb_strlen($word) < 2) { continue; } $wordFreq[$word] = ($wordFreq[$word] ?? 0) + 1; } // 按词频排序,取前10个作为关键词 arsort($wordFreq); $topKeywords = array_slice(array_keys($wordFreq), 0, 10); // 合并客户指定的技能要求 $skillTags = array_unique(array_merge( $topKeywords, $demand['required_skills'] ?? [] )); return [ 'keywords' => $skillTags, 'original_text' => $text, 'word_frequency' => $wordFreq ]; } /** * 构建程序员画像 * @param array $programmer 程序员数据 * @return array 程序员画像 */ private function buildProgrammerProfile($programmer) { // 计算综合评分 $compositeScore = $this->calculateCompositeScore($programmer); return [ 'skill_tags' => $programmer['skills'] ?? [], 'experience_years' => $programmer['experience_years'] ?? 0, 'composite_score' => $compositeScore, 'completion_rate' => $programmer['completion_rate'] ?? 0, 'avg_rating' => $programmer['avg_rating'] ?? 0, 'reputation_score' => $programmer['reputation_score'] ?? 0, 'project_count' => $programmer['project_count'] ?? 0 ]; } /** * 计算程序员综合评分 * @param array $programmer 程序员数据 * @return float 综合评分 */ private function calculateCompositeScore($programmer) { $weights = [ 'experience' => 0.3, 'completion_rate' => 0.25, 'avg_rating' => 0.25, 'reputation' => 0.2 ]; $score = 0; $score += ($programmer['experience_years'] ?? 0) / 10 * $weights['experience']; // 假设10年为满分 $score += ($programmer['completion_rate'] ?? 0) * $weights['completion_rate']; $score += (($programmer['avg_rating'] ?? 0) / 5) * $weights['avg_rating']; // 5分制归一化 $score += ($programmer['reputation_score'] ?? 0) * $weights['reputation']; return min(1.0, $score); // 确保不超过1.0 } /** * 计算匹配度 * @param array $demandProfile 需求画像 * @param array $programmerProfile 程序员画像 * @return float 匹配度分数 */ private function calculateMatchScore($demandProfile, $programmerProfile) { // 1. 技能相似度计算 $skillSimilarity = $this->jaccardSimilarity( $demandProfile['keywords'], $programmerProfile['skill_tags'] ); // 2. 经验系数计算 $experienceFactor = $this->normalizeExperience($programmerProfile['experience_years']); // 3. 综合评分 $compositeScore = $programmerProfile['composite_score']; // 4. 信誉系数 $reputationFactor = $programmerProfile['reputation_score']; // 5. 加权融合 return $this->combineFactorsWithWeights([ 'skill' => $skillSimilarity, 'composite' => $compositeScore, 'experience' => $experienceFactor, 'reputation' => $reputationFactor ]); } /** * Jaccard相似度算法 * @param array $arrayA 数组A * @param array $arrayB 数组B * @return float 相似度 */ private function jaccardSimilarity($arrayA, $arrayB) { if (empty($arrayA) || empty($arrayB)) { return 0; } $intersection = count(array_intersect($arrayA, $arrayB)); $union = count(array_unique(array_merge($arrayA, $arrayB))); return $union > 0 ? $intersection / $union : 0; } /** * 经验系数归一化 * @param int $experienceYears 经验年数 * @return float 归一化后的经验系数 */ private function normalizeExperience($experienceYears) { // 使用对数函数归一化,防止经验年数过高时权重过大 return min(1.0, log($experienceYears + 1) / log(11)); // 10年经验为1.0 } /** * 加权融合各个因素 * @param array $factors 各因素得分 * @return float 最终得分 */ private function combineFactorsWithWeights($factors) { $totalScore = 0; foreach ($factors as $factor => $score) { $totalScore += $score * ($this->weights[$factor] ?? 0); } return $totalScore; } /** * 获取匹配详情 * @param array $demandProfile 需求画像 * @param array $programmerProfile 程序员画像 * @return array 匹配详情 */ private function getMatchDetails($demandProfile, $programmerProfile) { $skillSimilarity = $this->jaccardSimilarity( $demandProfile['keywords'], $programmerProfile['skill_tags'] ); return [ 'skill_similarity' => $skillSimilarity, 'composite_score' => $programmerProfile['composite_score'], 'experience_factor' => $this->normalizeExperience($programmerProfile['experience_years']), 'reputation_factor' => $programmerProfile['reputation_score'], 'matched_skills' => array_intersect($demandProfile['keywords'], $programmerProfile['skill_tags']) ]; } /** * 简单中文分词(用于演示,实际应使用jieba) * @param string $text 文本 * @return array 分词结果 */ private function simpleChineseSegment($text) { // 移除标点符号,按空格和常见分隔符分割 $text = preg_replace('/[^\w\x{4e00}-\x{9fa5}]/u', ' ', $text); return preg_split('/\s+/', $text, -1, PREG_SPLIT_NO_EMPTY); } /** * 加载停用词表 */ private function loadStopWords() { // 这里应该从文件加载停用词表,这里用常见停用词示例 $this->stopWords = [ '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这', '那', '他', '她', '它', '我们', '你们', '他们', '这个', '那个', '这些', '那些', '什么', '怎么', '为什么', '如何', '可以', '应该', '需要', '因为', '所以', '但是', '然后', '如果', '虽然', '尽管', '为了', '通过', '根据', '按照', '关于', '对于', '由于' ]; } /** * 设置权重配置 * @param array $weights 权重配置 */ public function setWeights($weights) { $this->weights = array_merge($this->weights, $weights); } /** * 获取当前权重配置 * @return array 权重配置 */ public function getWeights() { return $this->weights; } }