Files
work_dhd_back_end/app/service/MatchService.php
2026-01-06 11:23:52 +08:00

927 lines
32 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
declare (strict_types = 1);
namespace app\service;
use think\facade\Db;
/**
* 岗位简历匹配度计算服务
* 采用硬性条件一票否决 + 软性条件加分机制
*/
class MatchService
{
/**
* 计算岗位和简历的匹配度
* @param array $position 岗位信息
* @param array $resume 简历信息
* @return int 匹配度分数0-100分硬性条件不满足返回0分
*/
public function calculateMatchScore(array $position, array $resume): int
{
// 第一步:检查硬性条件(一票否决)
$hardCheck = $this->checkHardRequirements($position, $resume);
if (!$hardCheck['passed']) {
return 0; // 硬性条件不满足直接返回0分
}
// 第二步计算软性条件匹配度100分制
$softCheck = $this->calculateSoftRequirements($position, $resume);
return $softCheck['score'];
}
/**
* 批量匹配查询(基于数据库)
* @param int $userId 用户ID
* @param int $page 页码从1开始
* @param int $pageSize 每页数量
* @param bool $filterZero 是否过滤0分岗位
* @return array
*/
public function batchMatchFromDb(int $userId, int $page = 1, int $pageSize = 20, bool $filterZero = false): array
{
// 1. 从数据库获取用户简历信息
$resume = $this->getUserResumeFromDb($userId);
if (empty($resume)) {
return [
'list' => [],
'pagination' => [
'page' => $page,
'page_size' => $pageSize,
'total' => 0,
'total_pages' => 0
]
];
}
// 2. 数据库快速过滤岗位
$filteredPositions = $this->filterPositionsFromDb($resume);
// 3. 计算匹配度
$results = [];
foreach ($filteredPositions as $position) {
$score = $this->calculateMatchScore($position, $resume);
if ($filterZero && $score == 0) {
continue; // 过滤0分岗位
}
$results[] = [
'position_id' => $position['id'],
'match_score' => $score,
'position' => $position
];
}
// 4. 按匹配度降序排序
usort($results, function($a, $b) {
return $b['match_score'] - $a['match_score'];
});
// 5. 分页
$total = count($results);
$totalPages = (int)ceil($total / $pageSize);
$offset = ($page - 1) * $pageSize;
$paginatedList = array_slice($results, $offset, $pageSize);
return [
'list' => $paginatedList,
'pagination' => [
'page' => $page,
'page_size' => $pageSize,
'total' => $total,
'total_pages' => $totalPages,
'has_more' => $page < $totalPages
]
];
}
/**
* 从数据库获取用户简历信息
* @param int $userId 用户ID
* @return array
*/
private function getUserResumeFromDb(int $userId): array
{
// 获取用户基本信息
$user = Db::name('t_user')->where('uid', $userId)->find();
if (empty($user)) {
return [];
}
// 构建简历数据结构
$resume = [
'user_id' => $userId,
'birth_date' => $user['birth_date'] ?? '',
'gender' => $user['gender'] ?? '',
'ethnicity' => $user['ethnicity'] ?? '',
'political_status' => $user['political_status'] ?? '',
'work_experience' => $user['work_experience'] ?? '',
'education' => []
];
// 获取教育经历(假设有教育经历表,表名可能是 t_user_education 或类似)
// 先尝试常见的表名
$educationTables = ['t_user_education', 'user_education', 't_education', 'education'];
$educations = [];
foreach ($educationTables as $tableName) {
try {
$educations = Db::name($tableName)->where('user_id', $userId)->select()->toArray();
if (!empty($educations)) {
break;
}
} catch (\Exception $e) {
// 表不存在,继续尝试下一个
continue;
}
}
// 如果没有找到教育经历表尝试从用户表的JSON字段获取
if (empty($educations) && isset($user['education'])) {
$educationData = is_string($user['education']) ? json_decode($user['education'], true) : $user['education'];
if (is_array($educationData)) {
$educations = $educationData;
}
}
$resume['education'] = $educations;
return $resume;
}
/**
* 从数据库快速过滤岗位
* @param array $resume 简历信息
* @return array
*/
private function filterPositionsFromDb(array $resume): array
{
$query = Db::name('no_notice_position')
->where('deleted_at', null); // 排除已删除的岗位
// 计算年龄
$age = 0;
if (!empty($resume['birth_date'])) {
$age = $this->calculateAge($resume['birth_date']);
}
// 学历过滤(如果简历有学历信息)
if (!empty($resume['education'])) {
$highestEducation = $this->getHighestEducation($resume['education']);
$educationLevel = $highestEducation['education_level'] ?? '';
// 学历等级映射
$educationLevels = [
'普通本科' => 3,
'本科' => 3,
'大学本科' => 3,
'本科学历' => 3,
'硕士研究生' => 4,
'硕士' => 4,
'研究生' => 4,
'博士研究生' => 5,
'博士' => 5,
];
$actualLevel = $educationLevels[$educationLevel] ?? 0;
// 学历要求过滤:岗位要求的学历等级 <= 简历实际学历等级
// 这里简化处理,实际可以根据数据库中的具体值调整
if ($actualLevel >= 3) {
// 如果是本科及以上,可以匹配"本科"、"本科及以上"等要求
$query->where(function($q) {
$q->where('education_require', 'like', '%本科%')
->orWhere('education_require', 'like', '%硕士%')
->orWhere('education_require', 'like', '%博士%')
->orWhere('education_require', '=', '')
->orWhereNull('education_require');
});
}
}
// 年龄过滤(年龄要求是文本格式,如"18周岁以上、35周岁以下"
// 这里先不过滤在详细匹配时再检查因为文本格式难以用SQL精确匹配
// 如果需要优化,可以在数据库中添加 age_min 和 age_max 字段
// 性别过滤
if (!empty($resume['gender'])) {
$query->where(function($q) use ($resume) {
$q->where('sex_require', '不限制')
->orWhere('sex_require', $resume['gender'])
->orWhere('sex_require', '')
->orWhereNull('sex_require');
});
}
// 获取过滤后的岗位
$positions = $query->select()->toArray();
// 解析JSON字段
foreach ($positions as &$position) {
if (!empty($position['position_other_require'])) {
$otherRequire = is_string($position['position_other_require'])
? json_decode($position['position_other_require'], true)
: $position['position_other_require'];
// 将JSON数据合并到position_require中
$position['position_require'] = [
'学历要求' => $position['education_require'] ?? '',
'学位要求' => $position['degree_require'] ?? '',
'年龄要求' => $position['age_require'] ?? '',
'性别要求' => $position['sex_require'] ?? '',
'专业(学科)类别' => $otherRequire['专业(学科)类别'] ?? '',
'专业-本科' => $otherRequire['专业-本科'] ?? '',
'专业-硕士' => $otherRequire['专业-硕士'] ?? '',
'其他资格条件' => $otherRequire['其他资格条件'] ?? '',
'专业资格条件' => $otherRequire['专业资格条件'] ?? '',
];
} else {
$position['position_require'] = [
'学历要求' => $position['education_require'] ?? '',
'学位要求' => $position['degree_require'] ?? '',
'年龄要求' => $position['age_require'] ?? '',
'性别要求' => $position['sex_require'] ?? '',
];
}
}
return $positions;
}
/**
* 检查硬性条件(一票否决机制)
* @param array $position 岗位信息
* @param array $resume 简历信息
* @return array
*/
private function checkHardRequirements(array $position, array $resume): array
{
$positionRequire = $position['position_require'] ?? [];
$result = [
'passed' => true,
'rejection_reasons' => [],
'details' => []
];
// 1. 学历要求(硬性)
if (!empty($positionRequire['学历要求'])) {
$educationCheck = $this->checkEducation($positionRequire['学历要求'], $resume);
$result['details']['学历要求'] = $educationCheck;
if (!$educationCheck['passed']) {
$result['passed'] = false;
$result['rejection_reasons'][] = $educationCheck['reason'];
}
}
// 2. 学位要求(硬性)
if (!empty($positionRequire['学位要求'])) {
$degreeCheck = $this->checkDegree($positionRequire['学位要求'], $resume);
$result['details']['学位要求'] = $degreeCheck;
if (!$degreeCheck['passed']) {
$result['passed'] = false;
$result['rejection_reasons'][] = $degreeCheck['reason'];
}
}
// 3. 年龄要求(硬性)
if (!empty($positionRequire['年龄要求'])) {
$ageCheck = $this->checkAge($positionRequire['年龄要求'], $resume);
$result['details']['年龄要求'] = $ageCheck;
if (!$ageCheck['passed']) {
$result['passed'] = false;
$result['rejection_reasons'][] = $ageCheck['reason'];
}
}
// 4. 专业要求(硬性)
// 支持多种专业字段格式:专业(学科)类别、专业-本科、专业-硕士
$majorRequire = $this->getMajorRequirement($positionRequire, $resume);
if (!empty($majorRequire)) {
$majorCheck = $this->checkMajor($majorRequire, $resume);
$result['details']['专业要求'] = $majorCheck;
if (!$majorCheck['passed']) {
$result['passed'] = false;
$result['rejection_reasons'][] = $majorCheck['reason'];
}
}
// 5. 性别要求(硬性,如果明确要求)
$otherConditions = $positionRequire['其他资格条件'] ?? '';
if (preg_match('/适合(男|女)性/u', $otherConditions, $matches)) {
$genderCheck = $this->checkGender($matches[1], $resume);
$result['details']['性别要求'] = $genderCheck;
if (!$genderCheck['passed']) {
$result['passed'] = false;
$result['rejection_reasons'][] = $genderCheck['reason'];
}
}
return $result;
}
/**
* 计算软性条件匹配度100分制
* @param array $position 岗位信息
* @param array $resume 简历信息
* @return array
*/
private function calculateSoftRequirements(array $position, array $resume): array
{
$positionRequire = $position['position_require'] ?? [];
$score = 0;
$details = [];
$maxScore = 100;
// 1. 专业匹配度40分- 即使专业类别符合,也可以根据专业相关性打分
$majorScore = $this->scoreMajorMatch($positionRequire['专业(学科)类别'] ?? '', $resume);
$score += $majorScore['score'];
$details['专业匹配度'] = $majorScore;
// 2. 学历层次匹配度20分- 超过要求的学历可以加分
$educationScore = $this->scoreEducationLevel($positionRequire['学历要求'] ?? '', $resume);
$score += $educationScore['score'];
$details['学历层次匹配度'] = $educationScore;
// 3. 专业资格条件20分- 竞赛获奖等
$qualificationScore = $this->scoreQualification($positionRequire['专业资格条件'] ?? '', $resume);
$score += $qualificationScore['score'];
$details['专业资格条件'] = $qualificationScore;
// 4. 基层工作经历10分
$workExpScore = $this->scoreWorkExperience($resume);
$score += $workExpScore['score'];
$details['基层工作经历'] = $workExpScore;
// 5. 其他条件匹配10分- 如政治面貌、特殊身份等
$otherScore = $this->scoreOtherConditions($positionRequire['其他资格条件'] ?? '', $resume);
$score += $otherScore['score'];
$details['其他条件'] = $otherScore;
return [
'score' => min(100, $score),
'max_score' => $maxScore,
'details' => $details
];
}
// ==================== 硬性条件检查方法 ====================
/**
* 检查学历要求(硬性)
*/
private function checkEducation(string $requirement, array $resume): array
{
$educations = $resume['education'] ?? [];
if (empty($educations)) {
return [
'passed' => false,
'reason' => '未提供学历信息',
'required' => $requirement,
'actual' => null
];
}
$highestEducation = $this->getHighestEducation($educations);
if (empty($highestEducation)) {
return [
'passed' => false,
'reason' => '无法识别最高学历',
'required' => $requirement,
'actual' => null
];
}
$educationLevel = $highestEducation['education_level'] ?? '';
$educationLevels = [
'普通本科' => 3,
'本科' => 3,
'大学本科' => 3,
'本科学历' => 3,
'硕士研究生' => 4,
'硕士' => 4,
'研究生' => 4,
'博士研究生' => 5,
'博士' => 5,
];
$requireLevel = $this->parseEducationRequire($requirement);
$actualLevel = $educationLevels[$educationLevel] ?? 0;
if ($actualLevel >= $requireLevel) {
return [
'passed' => true,
'required' => $requirement,
'actual' => $educationLevel
];
}
return [
'passed' => false,
'reason' => "学历不符合要求:需要{$requirement},实际为{$educationLevel}",
'required' => $requirement,
'actual' => $educationLevel
];
}
/**
* 检查学位要求(硬性)
*/
private function checkDegree(string $requirement, array $resume): array
{
$educations = $resume['education'] ?? [];
if (empty($educations)) {
return [
'passed' => false,
'reason' => '未提供学位信息',
'required' => $requirement,
'actual' => null
];
}
$highestEducation = $this->getHighestEducation($educations);
$degree = $highestEducation['degree'] ?? '';
$degreeLevels = [
'学士' => 1,
'硕士' => 2,
'博士' => 3,
];
$requireLevel = $this->parseDegreeRequire($requirement);
$actualLevel = $degreeLevels[$degree] ?? 0;
if ($actualLevel >= $requireLevel) {
return [
'passed' => true,
'required' => $requirement,
'actual' => $degree
];
}
return [
'passed' => false,
'reason' => "学位不符合要求:需要{$requirement},实际为{$degree}",
'required' => $requirement,
'actual' => $degree
];
}
/**
* 检查年龄要求(硬性)
*/
private function checkAge(string $requirement, array $resume): array
{
// 如果年龄要求为空或"无",直接通过
$requirement = trim($requirement);
if (empty($requirement) || $requirement === '无' || $requirement === '不限制') {
return [
'passed' => true,
'required' => $requirement ?: '无要求',
'actual' => '无要求'
];
}
$birthDate = $resume['birth_date'] ?? '';
if (empty($birthDate)) {
return [
'passed' => false,
'reason' => '未提供出生日期',
'required' => $requirement,
'actual' => null
];
}
$age = $this->calculateAge($birthDate);
if (preg_match('/(\d+)周岁以上.*?(\d+)周岁以下/u', $requirement, $matches)) {
$minAge = (int)$matches[1];
$maxAge = (int)$matches[2];
if ($age >= $minAge && $age <= $maxAge) {
return [
'passed' => true,
'required' => $requirement,
'actual' => "{$age}"
];
}
return [
'passed' => false,
'reason' => "年龄不符合要求:需要{$minAge}-{$maxAge}岁,实际为{$age}",
'required' => $requirement,
'actual' => "{$age}"
];
}
return [
'passed' => false,
'reason' => '无法解析年龄要求格式',
'required' => $requirement,
'actual' => "{$age}"
];
}
/**
* 检查专业要求(硬性)
*/
private function checkMajor(string $requirement, array $resume): array
{
$educations = $resume['education'] ?? [];
if (empty($educations)) {
return [
'passed' => false,
'reason' => '未提供专业信息',
'required' => $requirement,
'actual' => null
];
}
$matchedMajors = [];
foreach ($educations as $education) {
$majorName = $education['majors_name'] ?? '';
if ($this->isMajorCategoryMatch($majorName, $requirement)) {
$matchedMajors[] = $majorName;
}
}
if (!empty($matchedMajors)) {
return [
'passed' => true,
'required' => $requirement,
'actual' => implode('、', $matchedMajors)
];
}
$actualMajors = array_map(function($edu) {
return $edu['majors_name'] ?? '';
}, $educations);
$actualMajors = array_filter($actualMajors);
return [
'passed' => false,
'reason' => "专业不符合要求:需要{$requirement},实际为" . implode('、', $actualMajors),
'required' => $requirement,
'actual' => implode('、', $actualMajors)
];
}
/**
* 检查性别要求(硬性)
*/
private function checkGender(string $requireGender, array $resume): array
{
$gender = $resume['gender'] ?? '';
if ($gender === $requireGender) {
return [
'passed' => true,
'required' => $requireGender,
'actual' => $gender
];
}
return [
'passed' => false,
'reason' => "性别不符合要求:需要{$requireGender},实际为{$gender}",
'required' => $requireGender,
'actual' => $gender
];
}
// ==================== 软性条件评分方法 ====================
/**
* 专业匹配度评分40分
*/
private function scoreMajorMatch(string $requirement, array $resume): array
{
if (empty($requirement)) {
return ['score' => 40, 'reason' => '无专业要求'];
}
$educations = $resume['education'] ?? [];
if (empty($educations)) {
return ['score' => 0, 'reason' => '无专业信息'];
}
$maxScore = 0;
foreach ($educations as $education) {
$majorName = $education['majors_name'] ?? '';
$score = $this->calculateMajorRelevanceScore($majorName, $requirement);
$maxScore = max($maxScore, $score);
}
return [
'score' => min(40, $maxScore),
'max_score' => 40,
'reason' => $maxScore >= 40 ? '专业高度匹配' : '专业部分匹配'
];
}
/**
* 学历层次匹配度评分20分
*/
private function scoreEducationLevel(string $requirement, array $resume): array
{
$educations = $resume['education'] ?? [];
if (empty($educations)) {
return ['score' => 0, 'reason' => '无学历信息'];
}
$highestEducation = $this->getHighestEducation($educations);
$educationLevel = $highestEducation['education_level'] ?? '';
$educationLevels = [
'普通本科' => 3,
'本科' => 3,
'大学本科' => 3,
'本科学历' => 3,
'硕士研究生' => 4,
'硕士' => 4,
'研究生' => 4,
'博士研究生' => 5,
'博士' => 5,
];
$requireLevel = $this->parseEducationRequire($requirement);
$actualLevel = $educationLevels[$educationLevel] ?? 0;
// 刚好满足要求15分超过一级20分超过两级及以上20分
if ($actualLevel == $requireLevel) {
return ['score' => 15, 'max_score' => 20, 'reason' => '刚好满足要求'];
} elseif ($actualLevel > $requireLevel) {
$exceed = $actualLevel - $requireLevel;
return [
'score' => 15 + min(5, $exceed * 5),
'max_score' => 20,
'reason' => "超过要求{$exceed}"
];
}
return ['score' => 0, 'reason' => '未达到要求'];
}
/**
* 专业资格条件评分20分
*/
private function scoreQualification(string $requirement, array $resume): array
{
if (empty($requirement)) {
return [
'score' => 20,
'max_score' => 20,
'reason' => '无专业资格要求'
];
}
// 这里可以根据简历中的证书、获奖等信息来评分
// 由于当前简历数据结构中没有这些字段,暂时返回基础分数
// 实际应用中需要扩展简历数据结构
return [
'score' => 0,
'max_score' => 20,
'reason' => '未提供专业资格证明材料(需扩展简历数据结构)'
];
}
/**
* 基层工作经历评分10分
*/
private function scoreWorkExperience(array $resume): array
{
$workExperience = $resume['work_experience'] ?? '';
if (empty($workExperience) || strpos($workExperience, '无') !== false) {
return [
'score' => 0,
'max_score' => 10,
'reason' => '无基层工作经历'
];
}
// 可以根据工作年限进一步细化评分
return [
'score' => 10,
'max_score' => 10,
'reason' => '有基层工作经历'
];
}
/**
* 其他条件评分10分
*/
private function scoreOtherConditions(string $requirement, array $resume): array
{
if (empty($requirement)) {
return ['score' => 10, 'reason' => '无其他条件要求'];
}
$score = 0;
$maxScore = 10;
// 政治面貌等可以根据岗位要求评分
// 当前暂不实现,返回基础分数
return [
'score' => 10,
'max_score' => $maxScore,
'reason' => '其他条件匹配(需根据具体岗位要求细化)'
];
}
// ==================== 辅助方法 ====================
/**
* 计算年龄
*/
private function calculateAge(string $birthDate): int
{
$birthTimestamp = strtotime($birthDate);
$currentTimestamp = time();
$age = date('Y', $currentTimestamp) - date('Y', $birthTimestamp);
if (date('md', $currentTimestamp) < date('md', $birthTimestamp)) {
$age--;
}
return $age;
}
/**
* 获取最高学历
*/
private function getHighestEducation(array $educations): ?array
{
if (empty($educations)) {
return null;
}
$educationLevels = [
'普通本科' => 3,
'本科' => 3,
'大学本科' => 3,
'本科学历' => 3,
'硕士研究生' => 4,
'硕士' => 4,
'研究生' => 4,
'博士研究生' => 5,
'博士' => 5,
];
$highest = null;
$highestLevel = 0;
foreach ($educations as $education) {
$level = $educationLevels[$education['education_level'] ?? ''] ?? 0;
if ($level > $highestLevel) {
$highestLevel = $level;
$highest = $education;
}
}
return $highest;
}
/**
* 解析学历要求
*/
private function parseEducationRequire(string $require): int
{
if (strpos($require, '本科') !== false) {
return 3;
}
if (strpos($require, '硕士') !== false) {
return 4;
}
if (strpos($require, '博士') !== false) {
return 5;
}
return 0;
}
/**
* 解析学位要求
*/
private function parseDegreeRequire(string $require): int
{
if (strpos($require, '学士') !== false) {
return 1;
}
if (strpos($require, '硕士') !== false) {
return 2;
}
if (strpos($require, '博士') !== false) {
return 3;
}
return 0;
}
/**
* 获取专业要求(支持多种格式)
*/
private function getMajorRequirement(array $positionRequire, array $resume): string
{
// 优先使用 专业(学科)类别
if (!empty($positionRequire['专业(学科)类别'])) {
return $positionRequire['专业(学科)类别'];
}
// 根据最高学历选择对应的专业要求
$educations = $resume['education'] ?? [];
if (!empty($educations)) {
$highestEducation = $this->getHighestEducation($educations);
$educationLevel = $highestEducation['education_level'] ?? '';
// 判断学历等级
$isUndergraduate = in_array($educationLevel, ['普通本科', '本科', '大学本科', '本科学历']);
$isGraduate = in_array($educationLevel, ['硕士研究生', '硕士', '研究生', '博士研究生', '博士']);
if ($isUndergraduate && !empty($positionRequire['专业-本科'])) {
return $positionRequire['专业-本科'];
}
if ($isGraduate && !empty($positionRequire['专业-硕士'])) {
return $positionRequire['专业-硕士'];
}
// 如果没有对应学历的专业要求,尝试使用另一个
if ($isGraduate && !empty($positionRequire['专业-本科'])) {
return $positionRequire['专业-本科'];
}
}
return '';
}
/**
* 判断专业类别是否匹配(硬性检查)
*/
private function isMajorCategoryMatch(string $majorName, string $majorRequire): bool
{
if (empty($majorName) || empty($majorRequire)) {
return false;
}
// 支持多种分隔符:,、、
$requireCategories = preg_split('/[,、,]/u', $majorRequire);
foreach ($requireCategories as $category) {
$category = trim($category);
// 直接匹配专业名称
if ($majorName === $category || strpos($majorName, $category) !== false || strpos($category, $majorName) !== false) {
return true;
}
// 计算机科学与技术类
if (strpos($category, '计算机') !== false) {
$computerKeywords = ['计算机', '软件', '网络', '信息', '数据', '人工智能', '大数据', '网络安全', '信息安全'];
foreach ($computerKeywords as $keyword) {
if (strpos($majorName, $keyword) !== false) {
return true;
}
}
}
// 电气、电子及自动化类
if (strpos($category, '电气') !== false || strpos($category, '电子') !== false || strpos($category, '自动化') !== false) {
$electronicsKeywords = ['电气', '电子', '自动化', '通信', '电信', '电子信息'];
foreach ($electronicsKeywords as $keyword) {
if (strpos($majorName, $keyword) !== false) {
return true;
}
}
}
// 教育学类
if (strpos($category, '教育') !== false) {
if (strpos($majorName, '教育') !== false) {
return true;
}
}
// 心理学类
if (strpos($category, '心理') !== false) {
if (strpos($majorName, '心理') !== false) {
return true;
}
}
}
return false;
}
/**
* 计算专业相关性分数(软性评分)
*/
private function calculateMajorRelevanceScore(string $majorName, string $majorRequire): int
{
if ($this->isMajorCategoryMatch($majorName, $majorRequire)) {
return 40; // 完全匹配
}
// 可以根据专业相似度进一步细化评分
// 这里简单返回0分
return 0;
}
}