1189 lines
44 KiB
PHP
1189 lines
44 KiB
PHP
<?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)) {
|
||
// 记录调试信息
|
||
error_log("获取用户简历失败 - 用户ID: {$userId}");
|
||
return [
|
||
'list' => [],
|
||
'pagination' => [
|
||
'page' => $page,
|
||
'page_size' => $pageSize,
|
||
'total' => 0,
|
||
'total_pages' => 0
|
||
],
|
||
'debug' => [
|
||
'error' => '用户不存在或简历信息为空',
|
||
'user_id' => $userId
|
||
]
|
||
];
|
||
}
|
||
|
||
// 2. 从数据库获取所有岗位
|
||
$filteredPositions = $this->filterPositionsFromDb($resume);
|
||
|
||
// 记录调试信息
|
||
error_log("获取到岗位数量: " . count($filteredPositions) . ", 用户ID: {$userId}");
|
||
|
||
if (empty($filteredPositions)) {
|
||
return [
|
||
'list' => [],
|
||
'pagination' => [
|
||
'page' => $page,
|
||
'page_size' => $pageSize,
|
||
'total' => 0,
|
||
'total_pages' => 0
|
||
],
|
||
'debug' => [
|
||
'error' => '未找到任何岗位',
|
||
'user_id' => $userId,
|
||
'positions_count' => 0
|
||
]
|
||
];
|
||
}
|
||
|
||
// 3. 计算匹配度
|
||
$results = [];
|
||
$zeroScoreCount = 0;
|
||
$firstZeroScoreReason = null; // 记录第一个0分岗位的拒绝原因
|
||
foreach ($filteredPositions as $position) {
|
||
try {
|
||
// 检查硬性条件,获取详细的拒绝原因
|
||
$hardCheck = $this->checkHardRequirements($position, $resume);
|
||
$score = 0;
|
||
$rejectionInfo = null;
|
||
|
||
if ($hardCheck['passed']) {
|
||
// 硬性条件通过,计算软性条件分数
|
||
$softCheck = $this->calculateSoftRequirements($position, $resume);
|
||
$score = $softCheck['score'];
|
||
} else {
|
||
// 硬性条件不通过,记录拒绝原因
|
||
$score = 0;
|
||
$rejectionInfo = [
|
||
'rejection_reasons' => $hardCheck['rejection_reasons'],
|
||
'details' => $hardCheck['details']
|
||
];
|
||
}
|
||
|
||
if ($score == 0) {
|
||
$zeroScoreCount++;
|
||
// 记录第一个0分岗位的拒绝原因
|
||
if ($firstZeroScoreReason === null && $rejectionInfo !== null) {
|
||
$firstZeroScoreReason = [
|
||
'position_id' => $position['id'] ?? 0,
|
||
'position_name' => $position['position_name'] ?? $position['name'] ?? '',
|
||
'rejection_reasons' => $rejectionInfo['rejection_reasons'],
|
||
'details' => $rejectionInfo['details']
|
||
];
|
||
}
|
||
}
|
||
|
||
if ($filterZero && $score == 0) {
|
||
continue; // 过滤0分岗位
|
||
}
|
||
|
||
$results[] = [
|
||
'position_id' => $position['id'] ?? 0,
|
||
'match_score' => $score,
|
||
'position' => $position
|
||
];
|
||
} catch (\Exception $e) {
|
||
// 如果计算出错,记录错误但继续处理其他岗位
|
||
error_log("计算匹配度失败 - 岗位ID: " . ($position['id'] ?? 'unknown') . ", 错误: " . $e->getMessage());
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 记录调试信息
|
||
error_log("匹配完成 - 总岗位数: " . count($filteredPositions) . ", 有效结果数: " . count($results) . ", 0分岗位数: {$zeroScoreCount}, filter_zero: " . ($filterZero ? 'true' : 'false'));
|
||
|
||
// 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
|
||
],
|
||
'debug' => [
|
||
'total_positions' => count($filteredPositions),
|
||
'zero_score_count' => $zeroScoreCount,
|
||
'filter_zero' => $filterZero,
|
||
'result_count' => $total,
|
||
'resume_info' => [
|
||
'user_id' => $resume['user_id'] ?? 0,
|
||
'has_education' => !empty($resume['education']),
|
||
'education_count' => count($resume['education'] ?? []),
|
||
'birth_date' => $resume['birth_date'] ?? '',
|
||
'gender' => $resume['gender'] ?? '',
|
||
'education_details' => array_map(function($edu) {
|
||
return [
|
||
'education_level' => $edu['education_level'] ?? '',
|
||
'degree' => $edu['degree'] ?? '',
|
||
'majors_name' => $edu['majors_name'] ?? '',
|
||
'majors_code' => $edu['majors_code'] ?? '',
|
||
'majors_category' => $edu['majors_category'] ?? '',
|
||
'school_name' => $edu['school_name'] ?? '',
|
||
];
|
||
}, $resume['education'] ?? []),
|
||
'education_raw' => array_map(function($edu) {
|
||
// 返回原始数据的前几个字段用于调试(避免数据过大)
|
||
$raw = $edu['_raw'] ?? [];
|
||
// 只返回前15个字段
|
||
return array_slice($raw, 0, 15, true);
|
||
}, $resume['education'] ?? []),
|
||
],
|
||
'first_zero_reason' => $firstZeroScoreReason
|
||
]
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 从数据库获取用户简历信息
|
||
* @param int $userId 用户ID
|
||
* @return array
|
||
*/
|
||
private function getUserResumeFromDb(int $userId): array
|
||
{
|
||
// 1. 获取用户基本信息(t_user表)
|
||
// 尝试不同的主键字段名
|
||
$user = null;
|
||
try {
|
||
$user = Db::name('t_user')->where('uid', $userId)->find();
|
||
} catch (\Exception $e) {
|
||
// ignore and fallback below
|
||
}
|
||
// 如果按 uid 没有查到,再按 id 尝试(即使没有异常也尝试,以防字段是 id)
|
||
if (empty($user)) {
|
||
try {
|
||
$user = Db::name('t_user')->where('id', $userId)->find();
|
||
} catch (\Exception $e2) {
|
||
error_log("查询用户失败 - 用户ID: {$userId}, 错误: " . $e2->getMessage());
|
||
}
|
||
}
|
||
|
||
if (empty($user)) {
|
||
error_log("用户不存在 - 用户ID: {$userId}");
|
||
return [];
|
||
}
|
||
|
||
// 转换为数组(如果返回的是对象)
|
||
if (is_object($user)) {
|
||
$user = $user->toArray();
|
||
}
|
||
|
||
// 2. 获取用户简历信息(t_user_curriculum_vitae表)
|
||
// 尝试使用uid或user_id字段
|
||
$curriculumVitae = null;
|
||
try {
|
||
$curriculumVitae = Db::name('t_user_curriculum_vitae')
|
||
->where('uid', $userId)
|
||
->find();
|
||
} catch (\Exception $e) {
|
||
// ignore and fallback below
|
||
}
|
||
// 如果按 uid 没查到,再按 user_id 查一次
|
||
if (empty($curriculumVitae)) {
|
||
try {
|
||
$curriculumVitae = Db::name('t_user_curriculum_vitae')
|
||
->where('user_id', $userId)
|
||
->find();
|
||
} catch (\Exception $e2) {
|
||
// 如果都不存在,curriculumVitae保持为null
|
||
}
|
||
}
|
||
|
||
// 转换为数组
|
||
if (is_object($curriculumVitae)) {
|
||
$curriculumVitae = $curriculumVitae->toArray();
|
||
} elseif ($curriculumVitae === null) {
|
||
$curriculumVitae = [];
|
||
}
|
||
|
||
// 3. 获取用户教育经历(t_user_education表)
|
||
// 尝试使用uid或user_id字段
|
||
$educations = [];
|
||
try {
|
||
$educations = Db::name('t_user_education')
|
||
->where('uid', $userId)
|
||
->order('education_level desc') // 按学历等级降序,最高学历在前
|
||
->select()
|
||
->toArray();
|
||
} catch (\Exception $e) {
|
||
// ignore and fallback below
|
||
}
|
||
// 如果按 uid 没查到,再按 user_id 查一次(即使没有异常也尝试,以防字段是 user_id)
|
||
if (empty($educations)) {
|
||
try {
|
||
$educations = Db::name('t_user_education')
|
||
->where('user_id', $userId)
|
||
->order('education_level desc')
|
||
->select()
|
||
->toArray();
|
||
} catch (\Exception $e2) {
|
||
// 如果都不存在,educations保持为空数组
|
||
}
|
||
}
|
||
|
||
// 3.1 如果教育经历里有专业代码,预先加载专业表(da_majors)以便解析专业名称
|
||
$majorCodeMap = [];
|
||
$majorCodes = [];
|
||
foreach ($educations as $education) {
|
||
// 收集所有可能的专业代码字段
|
||
$codeFields = ['major_code', 'majors_code', 'code', 'major_id', 'majors_id', 'major_no', 'id'];
|
||
foreach ($codeFields as $field) {
|
||
if (!empty($education[$field])) {
|
||
$majorCodes[] = $education[$field];
|
||
}
|
||
}
|
||
}
|
||
$majorCodes = array_values(array_unique($majorCodes));
|
||
|
||
if (!empty($majorCodes)) {
|
||
try {
|
||
// 尝试多种可能的查询字段:id, code, major_code, majors_id
|
||
$majors = [];
|
||
$queryFields = ['id', 'code', 'major_code', 'majors_id', 'major_id'];
|
||
|
||
foreach ($queryFields as $queryField) {
|
||
try {
|
||
$majors = Db::name('da_majors')
|
||
->whereIn($queryField, $majorCodes)
|
||
->select()
|
||
->toArray();
|
||
if (!empty($majors)) {
|
||
error_log("成功从da_majors表查询到 " . count($majors) . " 条专业记录,使用字段: {$queryField}");
|
||
break; // 找到匹配的字段,退出循环
|
||
}
|
||
} catch (\Exception $e) {
|
||
// 字段不存在,继续尝试下一个
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 构建专业代码映射表
|
||
foreach ($majors as $major) {
|
||
// 尝试多种可能的代码字段
|
||
$code = $major['id'] ?? $major['code'] ?? $major['major_code'] ?? $major['majors_id'] ?? $major['major_id'] ?? null;
|
||
if (!$code) {
|
||
continue;
|
||
}
|
||
|
||
// 尝试多种可能的名称字段
|
||
$name = $major['major_name'] ?? $major['name'] ?? $major['title'] ?? $major['专业名称'] ?? '';
|
||
|
||
// 尝试多种可能的类别字段
|
||
$category = $major['category_name'] ?? $major['category'] ?? $major['major_category'] ?? $major['专业类别'] ?? $major['学科类别'] ?? '';
|
||
|
||
$majorCodeMap[$code] = [
|
||
'name' => $name,
|
||
'category' => $category,
|
||
];
|
||
}
|
||
|
||
error_log("专业代码映射表构建完成,共 " . count($majorCodeMap) . " 条记录");
|
||
} catch (\Exception $e) {
|
||
error_log("查询da_majors表失败: " . $e->getMessage());
|
||
}
|
||
} else {
|
||
error_log("未找到专业代码,无法查询da_majors表");
|
||
}
|
||
|
||
// 构建简历数据结构
|
||
$resume = [
|
||
'user_id' => $userId,
|
||
// 从用户表获取基本信息
|
||
'birth_date' => $user['birth_date'] ?? $curriculumVitae['birth_date'] ?? '',
|
||
'gender' => $user['gender'] ?? $curriculumVitae['gender'] ?? '',
|
||
'ethnicity' => $user['ethnicity'] ?? $curriculumVitae['ethnicity'] ?? '',
|
||
'political_status' => $user['political_status'] ?? $curriculumVitae['political_status'] ?? '',
|
||
// 从简历表获取工作经历等信息
|
||
'work_experience' => $curriculumVitae['work_experience'] ?? $user['work_experience'] ?? '',
|
||
// 教育经历从t_user_education表获取
|
||
'education' => []
|
||
];
|
||
|
||
// 处理教育经历数据,确保字段名正确
|
||
foreach ($educations as $education) {
|
||
// 尝试多种可能的专业字段名
|
||
$majorName = '';
|
||
$possibleMajorFields = [
|
||
'majors_name', 'major_name', 'major', 'major_field',
|
||
'specialty', 'profession', 'discipline', 'subject',
|
||
'专业名称', '专业', '学科专业', '专业类别'
|
||
];
|
||
foreach ($possibleMajorFields as $field) {
|
||
if (!empty($education[$field])) {
|
||
$majorName = $education[$field];
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果有专业代码但没有名称,尝试从 da_majors 映射获取
|
||
if (empty($majorName)) {
|
||
$codeFields = ['major_code', 'majors_code', 'code', 'major_id', 'majors_id', 'major_no', 'id'];
|
||
foreach ($codeFields as $field) {
|
||
if (!empty($education[$field])) {
|
||
$codeValue = $education[$field];
|
||
// 尝试直接匹配
|
||
if (isset($majorCodeMap[$codeValue])) {
|
||
$majorName = $majorCodeMap[$codeValue]['name'] ?? '';
|
||
$education['major_category'] = $majorCodeMap[$codeValue]['category'] ?? '';
|
||
if (!empty($majorName)) {
|
||
error_log("从majorCodeMap获取到专业名称: {$majorName}, 代码: {$codeValue}");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 保存专业代码,便于后续调试
|
||
$majorCode = '';
|
||
foreach (['major_code', 'majors_code', 'code', 'major_id', 'majors_id', 'major_no'] as $field) {
|
||
if (!empty($education[$field])) {
|
||
$majorCode = $education[$field];
|
||
break;
|
||
}
|
||
}
|
||
|
||
$resume['education'][] = [
|
||
'education_level' => $education['education_level'] ?? $education['education'] ?? $education['学历'] ?? '',
|
||
'degree' => $education['degree'] ?? $education['学位'] ?? '',
|
||
'majors_name' => $majorName,
|
||
'majors_code' => $majorCode,
|
||
'majors_category' => $education['major_category'] ?? $education['专业类别'] ?? '',
|
||
'school_name' => $education['school_name'] ?? $education['school'] ?? $education['学校名称'] ?? '',
|
||
'graduation_date' => $education['graduation_date'] ?? $education['毕业时间'] ?? '',
|
||
// 保存原始数据用于调试
|
||
'_raw' => $education
|
||
];
|
||
}
|
||
|
||
return $resume;
|
||
}
|
||
|
||
/**
|
||
* 从数据库获取所有岗位(用于匹配)
|
||
* @param array $resume 简历信息(用于可选的快速过滤)
|
||
* @return array
|
||
*/
|
||
private function filterPositionsFromDb(array $resume): array
|
||
{
|
||
// 获取所有未删除的岗位(no_notice_position表)
|
||
// 注意:根据用户要求,需要匹配所有岗位,所以这里不做严格过滤
|
||
// 只排除已删除的岗位,其他过滤在详细匹配时进行
|
||
$query = Db::name('no_notice_position');
|
||
|
||
// 排除已删除的岗位(如果表中有deleted_at字段)
|
||
// 使用where条件,如果字段不存在,SQL会报错,但我们可以通过查询表结构来判断
|
||
// 为了安全,先尝试查询,如果失败则查询所有记录
|
||
try {
|
||
// 尝试添加deleted_at条件
|
||
$query->whereNull('deleted_at');
|
||
$positions = $query->select()->toArray();
|
||
error_log("从no_notice_position表获取到 " . count($positions) . " 个岗位(已排除deleted_at)");
|
||
} catch (\Exception $e) {
|
||
// 如果字段不存在或其他错误,查询所有记录
|
||
try {
|
||
$query = Db::name('no_notice_position');
|
||
$positions = $query->select()->toArray();
|
||
error_log("从no_notice_position表获取到 " . count($positions) . " 个岗位(无deleted_at字段)");
|
||
} catch (\Exception $e2) {
|
||
error_log("查询岗位失败: " . $e2->getMessage());
|
||
$positions = [];
|
||
}
|
||
}
|
||
|
||
// 解析JSON字段,构建position_require结构
|
||
foreach ($positions as &$position) {
|
||
// 处理position_other_require字段(可能是JSON格式)
|
||
$otherRequire = [];
|
||
if (!empty($position['position_other_require'])) {
|
||
if (is_string($position['position_other_require'])) {
|
||
$otherRequire = json_decode($position['position_other_require'], true) ?: [];
|
||
} else {
|
||
$otherRequire = $position['position_other_require'];
|
||
}
|
||
}
|
||
|
||
// 构建统一的position_require结构
|
||
$position['position_require'] = [
|
||
'学历要求' => $position['education_require'] ?? '',
|
||
'学位要求' => $position['degree_require'] ?? '',
|
||
'年龄要求' => $position['age_require'] ?? '',
|
||
'性别要求' => $position['sex_require'] ?? '',
|
||
'专业(学科)类别' => $otherRequire['专业(学科)类别'] ?? '',
|
||
'专业-本科' => $otherRequire['专业-本科'] ?? '',
|
||
'专业-硕士' => $otherRequire['专业-硕士'] ?? '',
|
||
'其他资格条件' => $otherRequire['其他资格条件'] ?? '',
|
||
'专业资格条件' => $otherRequire['专业资格条件'] ?? '',
|
||
];
|
||
}
|
||
|
||
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. 年龄要求(硬性)
|
||
$ageRequire = $positionRequire['年龄要求'] ?? '';
|
||
if (!empty($ageRequire) && trim($ageRequire) !== '' && trim($ageRequire) !== '无' && trim($ageRequire) !== '不限制') {
|
||
$ageCheck = $this->checkAge($ageRequire, $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. 性别要求(硬性,如果明确要求)
|
||
// 优先检查 sex_require 字段
|
||
$sexRequire = $positionRequire['性别要求'] ?? '';
|
||
if (!empty($sexRequire) && $sexRequire !== '不限制' && $sexRequire !== '无') {
|
||
$genderCheck = $this->checkGender($sexRequire, $resume);
|
||
$result['details']['性别要求'] = $genderCheck;
|
||
if (!$genderCheck['passed']) {
|
||
$result['passed'] = false;
|
||
$result['rejection_reasons'][] = $genderCheck['reason'];
|
||
}
|
||
}
|
||
|
||
// 如果 sex_require 字段没有明确要求,再检查其他资格条件
|
||
if (empty($sexRequire) || $sexRequire === '不限制' || $sexRequire === '无') {
|
||
$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'] ??
|
||
$education['major_name'] ??
|
||
$education['major'] ??
|
||
$education['major_field'] ??
|
||
$education['specialty'] ??
|
||
$education['profession'] ??
|
||
$education['majors_category'] ?? '';
|
||
|
||
if (!empty($majorName) && $this->isMajorCategoryMatch($majorName, $requirement)) {
|
||
$matchedMajors[] = $majorName;
|
||
}
|
||
}
|
||
|
||
if (!empty($matchedMajors)) {
|
||
return [
|
||
'passed' => true,
|
||
'required' => $requirement,
|
||
'actual' => implode('、', $matchedMajors)
|
||
];
|
||
}
|
||
|
||
// 收集所有专业名称用于错误信息
|
||
$actualMajors = [];
|
||
foreach ($educations as $edu) {
|
||
$majorName = $edu['majors_name'] ??
|
||
$edu['major_name'] ??
|
||
$edu['major'] ??
|
||
$edu['major_field'] ??
|
||
$edu['specialty'] ??
|
||
$edu['profession'] ??
|
||
$edu['majors_category'] ?? '';
|
||
if (!empty($majorName)) {
|
||
$actualMajors[] = $majorName;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|