Files
code-bang/backend/application/common/service/DemandMatchService.php
T
2026-09-17 16:25:31 +08:00

299 lines
9.4 KiB
PHP

<?php
namespace app\common\service;
use think\Cache;
use think\Exception;
use app\common\traits\ServiceTrait;
class DemandMatchService
{
private $weights = [
'skill' => 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;
}
}