新增批量
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
APP_DEBUG = true
|
APP_DEBUG = true
|
||||||
|
|
||||||
DB_TYPE = mysql
|
DB_TYPE = mysql
|
||||||
DB_HOST = 127.0.0.1
|
DB_HOST = 192.168.28.18
|
||||||
DB_NAME = test
|
DB_NAME = dhd_official_test
|
||||||
DB_USER = username
|
DB_USER = dhd_official_test
|
||||||
DB_PASS = password
|
DB_PASS = 4zDsLWZaEzhPAGaf
|
||||||
DB_PORT = 3306
|
DB_PORT = 3306
|
||||||
DB_CHARSET = utf8
|
DB_CHARSET = utf8
|
||||||
|
|
||||||
|
|||||||
122
app/controller/Match.php
Normal file
122
app/controller/Match.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
namespace app\controller;
|
||||||
|
|
||||||
|
use app\BaseController;
|
||||||
|
use app\service\MatchService;
|
||||||
|
use think\response\Json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位简历匹配度控制器
|
||||||
|
*/
|
||||||
|
class MatchController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 计算岗位和简历的匹配度
|
||||||
|
* @return Json
|
||||||
|
*/
|
||||||
|
public function calculate(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 获取请求参数(支持JSON和表单数据)
|
||||||
|
$input = $this->request->param();
|
||||||
|
$position = $input['position'] ?? [];
|
||||||
|
$resume = $input['resume'] ?? [];
|
||||||
|
|
||||||
|
// 如果是JSON请求,尝试从JSON中获取
|
||||||
|
if (empty($position) && empty($resume)) {
|
||||||
|
$jsonData = json_decode($this->request->getContent(), true);
|
||||||
|
if (is_array($jsonData)) {
|
||||||
|
$position = $jsonData['position'] ?? [];
|
||||||
|
$resume = $jsonData['resume'] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($position) || empty($resume)) {
|
||||||
|
return json([
|
||||||
|
'code' => 400,
|
||||||
|
'msg' => '参数错误:岗位信息和简历信息不能为空',
|
||||||
|
'data' => null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算匹配度
|
||||||
|
$matchService = new MatchService();
|
||||||
|
$score = $matchService->calculateMatchScore($position, $resume);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '计算成功',
|
||||||
|
'data' => [
|
||||||
|
'match_score' => $score
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '计算失败:' . $e->getMessage(),
|
||||||
|
'data' => null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量匹配查询(基于数据库)
|
||||||
|
* @return Json
|
||||||
|
*/
|
||||||
|
public function batchMatch(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 获取请求参数
|
||||||
|
$input = $this->request->param();
|
||||||
|
$userId = $input['user_id'] ?? 0;
|
||||||
|
|
||||||
|
// 如果是JSON请求,尝试从JSON中获取
|
||||||
|
if (empty($userId)) {
|
||||||
|
$jsonData = json_decode($this->request->getContent(), true);
|
||||||
|
if (is_array($jsonData)) {
|
||||||
|
$userId = $jsonData['user_id'] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($userId) || !is_numeric($userId)) {
|
||||||
|
return json([
|
||||||
|
'code' => 400,
|
||||||
|
'msg' => '参数错误:user_id不能为空且必须为数字',
|
||||||
|
'data' => null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
$page = (int)($input['page'] ?? $jsonData['page'] ?? 1);
|
||||||
|
$pageSize = (int)($input['page_size'] ?? $jsonData['page_size'] ?? 20);
|
||||||
|
$filterZero = (bool)($input['filter_zero'] ?? $jsonData['filter_zero'] ?? false);
|
||||||
|
|
||||||
|
// 验证分页参数
|
||||||
|
if ($page < 1) $page = 1;
|
||||||
|
if ($pageSize < 1 || $pageSize > 100) $pageSize = 20; // 限制每页最多100条
|
||||||
|
|
||||||
|
// 批量匹配查询
|
||||||
|
$matchService = new MatchService();
|
||||||
|
$result = $matchService->batchMatchFromDb((int)$userId, $page, $pageSize, $filterZero);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '查询成功',
|
||||||
|
'data' => $result
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '查询失败:' . $e->getMessage(),
|
||||||
|
'data' => null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare (strict_types = 1);
|
|
||||||
|
|
||||||
namespace app\controller;
|
|
||||||
|
|
||||||
use app\BaseController;
|
|
||||||
use app\service\MatchService;
|
|
||||||
use think\response\Json;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 岗位简历匹配度控制器
|
|
||||||
*/
|
|
||||||
class MatchController extends BaseController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 计算岗位和简历的匹配度
|
|
||||||
* @return Json
|
|
||||||
*/
|
|
||||||
public function calculate(): Json
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
// 获取请求参数(支持JSON和表单数据)
|
|
||||||
$input = $this->request->param();
|
|
||||||
$position = $input['position'] ?? [];
|
|
||||||
$resume = $input['resume'] ?? [];
|
|
||||||
|
|
||||||
// 如果是JSON请求,尝试从JSON中获取
|
|
||||||
if (empty($position) && empty($resume)) {
|
|
||||||
$jsonData = json_decode($this->request->getContent(), true);
|
|
||||||
if (is_array($jsonData)) {
|
|
||||||
$position = $jsonData['position'] ?? [];
|
|
||||||
$resume = $jsonData['resume'] ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 参数验证
|
|
||||||
if (empty($position) || empty($resume)) {
|
|
||||||
return json([
|
|
||||||
'code' => 400,
|
|
||||||
'msg' => '参数错误:岗位信息和简历信息不能为空',
|
|
||||||
'data' => null
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算匹配度
|
|
||||||
$matchService = new MatchService();
|
|
||||||
$score = $matchService->calculateMatchScore($position, $resume);
|
|
||||||
|
|
||||||
return json([
|
|
||||||
'code' => 200,
|
|
||||||
'msg' => '计算成功',
|
|
||||||
'data' => [
|
|
||||||
'match_score' => $score
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return json([
|
|
||||||
'code' => 500,
|
|
||||||
'msg' => '计算失败:' . $e->getMessage(),
|
|
||||||
'data' => null
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -3,6 +3,8 @@ declare (strict_types = 1);
|
|||||||
|
|
||||||
namespace app\service;
|
namespace app\service;
|
||||||
|
|
||||||
|
use think\facade\Db;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 岗位简历匹配度计算服务
|
* 岗位简历匹配度计算服务
|
||||||
* 采用硬性条件一票否决 + 软性条件加分机制
|
* 采用硬性条件一票否决 + 软性条件加分机制
|
||||||
@@ -30,6 +32,225 @@ class MatchService
|
|||||||
return $softCheck['score'];
|
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 $position 岗位信息
|
||||||
@@ -76,8 +297,10 @@ class MatchService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. 专业要求(硬性)
|
// 4. 专业要求(硬性)
|
||||||
if (!empty($positionRequire['专业(学科)类别'])) {
|
// 支持多种专业字段格式:专业(学科)类别、专业-本科、专业-硕士
|
||||||
$majorCheck = $this->checkMajor($positionRequire['专业(学科)类别'], $resume);
|
$majorRequire = $this->getMajorRequirement($positionRequire, $resume);
|
||||||
|
if (!empty($majorRequire)) {
|
||||||
|
$majorCheck = $this->checkMajor($majorRequire, $resume);
|
||||||
$result['details']['专业要求'] = $majorCheck;
|
$result['details']['专业要求'] = $majorCheck;
|
||||||
if (!$majorCheck['passed']) {
|
if (!$majorCheck['passed']) {
|
||||||
$result['passed'] = false;
|
$result['passed'] = false;
|
||||||
@@ -174,8 +397,14 @@ class MatchService
|
|||||||
$educationLevel = $highestEducation['education_level'] ?? '';
|
$educationLevel = $highestEducation['education_level'] ?? '';
|
||||||
$educationLevels = [
|
$educationLevels = [
|
||||||
'普通本科' => 3,
|
'普通本科' => 3,
|
||||||
|
'本科' => 3,
|
||||||
|
'大学本科' => 3,
|
||||||
|
'本科学历' => 3,
|
||||||
'硕士研究生' => 4,
|
'硕士研究生' => 4,
|
||||||
|
'硕士' => 4,
|
||||||
|
'研究生' => 4,
|
||||||
'博士研究生' => 5,
|
'博士研究生' => 5,
|
||||||
|
'博士' => 5,
|
||||||
];
|
];
|
||||||
|
|
||||||
$requireLevel = $this->parseEducationRequire($requirement);
|
$requireLevel = $this->parseEducationRequire($requirement);
|
||||||
@@ -245,6 +474,16 @@ class MatchService
|
|||||||
*/
|
*/
|
||||||
private function checkAge(string $requirement, array $resume): array
|
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'] ?? '';
|
$birthDate = $resume['birth_date'] ?? '';
|
||||||
if (empty($birthDate)) {
|
if (empty($birthDate)) {
|
||||||
return [
|
return [
|
||||||
@@ -396,8 +635,14 @@ class MatchService
|
|||||||
|
|
||||||
$educationLevels = [
|
$educationLevels = [
|
||||||
'普通本科' => 3,
|
'普通本科' => 3,
|
||||||
|
'本科' => 3,
|
||||||
|
'大学本科' => 3,
|
||||||
|
'本科学历' => 3,
|
||||||
'硕士研究生' => 4,
|
'硕士研究生' => 4,
|
||||||
|
'硕士' => 4,
|
||||||
|
'研究生' => 4,
|
||||||
'博士研究生' => 5,
|
'博士研究生' => 5,
|
||||||
|
'博士' => 5,
|
||||||
];
|
];
|
||||||
|
|
||||||
$requireLevel = $this->parseEducationRequire($requirement);
|
$requireLevel = $this->parseEducationRequire($requirement);
|
||||||
@@ -514,8 +759,14 @@ class MatchService
|
|||||||
|
|
||||||
$educationLevels = [
|
$educationLevels = [
|
||||||
'普通本科' => 3,
|
'普通本科' => 3,
|
||||||
|
'本科' => 3,
|
||||||
|
'大学本科' => 3,
|
||||||
|
'本科学历' => 3,
|
||||||
'硕士研究生' => 4,
|
'硕士研究生' => 4,
|
||||||
|
'硕士' => 4,
|
||||||
|
'研究生' => 4,
|
||||||
'博士研究生' => 5,
|
'博士研究生' => 5,
|
||||||
|
'博士' => 5,
|
||||||
];
|
];
|
||||||
|
|
||||||
$highest = null;
|
$highest = null;
|
||||||
@@ -566,6 +817,41 @@ class MatchService
|
|||||||
return 0;
|
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 '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断专业类别是否匹配(硬性检查)
|
* 判断专业类别是否匹配(硬性检查)
|
||||||
*/
|
*/
|
||||||
@@ -575,11 +861,17 @@ class MatchService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$requireCategories = explode(',', $majorRequire);
|
// 支持多种分隔符:,、、
|
||||||
|
$requireCategories = preg_split('/[,、,]/u', $majorRequire);
|
||||||
|
|
||||||
foreach ($requireCategories as $category) {
|
foreach ($requireCategories as $category) {
|
||||||
$category = trim($category);
|
$category = trim($category);
|
||||||
|
|
||||||
|
// 直接匹配专业名称
|
||||||
|
if ($majorName === $category || strpos($majorName, $category) !== false || strpos($category, $majorName) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 计算机科学与技术类
|
// 计算机科学与技术类
|
||||||
if (strpos($category, '计算机') !== false) {
|
if (strpos($category, '计算机') !== false) {
|
||||||
$computerKeywords = ['计算机', '软件', '网络', '信息', '数据', '人工智能', '大数据', '网络安全', '信息安全'];
|
$computerKeywords = ['计算机', '软件', '网络', '信息', '数据', '人工智能', '大数据', '网络安全', '信息安全'];
|
||||||
@@ -599,6 +891,20 @@ class MatchService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 教育学类
|
||||||
|
if (strpos($category, '教育') !== false) {
|
||||||
|
if (strpos($majorName, '教育') !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心理学类
|
||||||
|
if (strpos($category, '心理') !== false) {
|
||||||
|
if (strpos($majorName, '心理') !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
93
check_db_structure.php
Normal file
93
check_db_structure.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
// 查看数据库表结构
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// 加载ThinkPHP框架
|
||||||
|
$app = new think\App();
|
||||||
|
$app->initialize();
|
||||||
|
|
||||||
|
use think\facade\Db;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置数据库配置(从.env读取或直接配置)
|
||||||
|
$dbConfig = [
|
||||||
|
'default' => 'mysql',
|
||||||
|
'connections' => [
|
||||||
|
'mysql' => [
|
||||||
|
'type' => 'mysql',
|
||||||
|
'hostname' => '192.168.28.18',
|
||||||
|
'database' => 'dhd_official_test',
|
||||||
|
'username' => 'dhd_official_test',
|
||||||
|
'password' => '4zDsLWZaEzhPAGaf',
|
||||||
|
'hostport' => '3306',
|
||||||
|
'charset' => 'utf8',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 设置配置
|
||||||
|
\think\facade\Config::set($dbConfig, 'database');
|
||||||
|
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "查看数据库表结构\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
|
||||||
|
// 查看职位表结构
|
||||||
|
echo "【职位表:no_notice_position】\n";
|
||||||
|
echo "----------------------------------------\n";
|
||||||
|
$positionColumns = Db::query("DESC no_notice_position");
|
||||||
|
foreach ($positionColumns as $col) {
|
||||||
|
$field = $col['Field'] ?? $col['field'] ?? '';
|
||||||
|
$type = $col['Type'] ?? $col['type'] ?? '';
|
||||||
|
$null = ($col['Null'] ?? $col['null'] ?? '') === 'YES' ? 'NULL' : 'NOT NULL';
|
||||||
|
echo sprintf("%-30s %-20s %s\n", $field, $type, $null);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n【职位表示例数据(前2条)】\n";
|
||||||
|
echo "----------------------------------------\n";
|
||||||
|
$positions = Db::name('no_notice_position')->limit(2)->select()->toArray();
|
||||||
|
foreach ($positions as $index => $pos) {
|
||||||
|
echo "记录 " . ($index + 1) . ":\n";
|
||||||
|
echo "ID: " . ($pos['id'] ?? '') . "\n";
|
||||||
|
echo "岗位名称: " . ($pos['positon_name'] ?? '') . "\n";
|
||||||
|
echo "学历要求: " . ($pos['education_require'] ?? '') . "\n";
|
||||||
|
echo "学位要求: " . ($pos['degree_require'] ?? '') . "\n";
|
||||||
|
echo "年龄要求: " . ($pos['age_require'] ?? '') . "\n";
|
||||||
|
echo "性别要求: " . ($pos['sex_require'] ?? '') . "\n";
|
||||||
|
echo "其他要求(JSON): " . ($pos['position_other_require'] ?? '') . "\n";
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n【用户表:t_user】\n";
|
||||||
|
echo "----------------------------------------\n";
|
||||||
|
$userColumns = Db::query("DESC t_user");
|
||||||
|
foreach ($userColumns as $col) {
|
||||||
|
$field = $col['Field'] ?? $col['field'] ?? '';
|
||||||
|
$type = $col['Type'] ?? $col['type'] ?? '';
|
||||||
|
$null = ($col['Null'] ?? $col['null'] ?? '') === 'YES' ? 'NULL' : 'NOT NULL';
|
||||||
|
echo sprintf("%-30s %-20s %s\n", $field, $type, $null);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n【用户表示例数据(前1条)】\n";
|
||||||
|
echo "----------------------------------------\n";
|
||||||
|
$users = Db::name('t_user')->limit(1)->select()->toArray();
|
||||||
|
if (!empty($users)) {
|
||||||
|
$user = $users[0];
|
||||||
|
echo "ID: " . ($user['id'] ?? '') . "\n";
|
||||||
|
echo "出生日期: " . ($user['birth_date'] ?? '') . "\n";
|
||||||
|
echo "性别: " . ($user['gender'] ?? '') . "\n";
|
||||||
|
// 只显示关键字段
|
||||||
|
$keyFields = ['id', 'birth_date', 'gender', 'education', 'majors_name'];
|
||||||
|
foreach ($keyFields as $field) {
|
||||||
|
if (isset($user[$field])) {
|
||||||
|
echo "$field: " . $user[$field] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "错误: " . $e->getMessage() . "\n";
|
||||||
|
echo "文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
## 接口地址
|
## 接口地址
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /match/calculate
|
POST http://work.dhdjy.com/match/calculate
|
||||||
```
|
```
|
||||||
|
|
||||||
## 接口说明
|
## 接口说明
|
||||||
|
|||||||
243
doc/快速匹配实现原理.md
Normal file
243
doc/快速匹配实现原理.md
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
# 快速匹配实现原理分析
|
||||||
|
|
||||||
|
## 为什么其他机构能"秒级"返回匹配结果?
|
||||||
|
|
||||||
|
### 核心原理:数据库层面快速过滤 + 只计算符合条件的岗位
|
||||||
|
|
||||||
|
## 实现方式对比
|
||||||
|
|
||||||
|
### ❌ 当前方案(慢的原因)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 接收1万个岗位的完整数据(JSON数组)
|
||||||
|
2. 在内存中遍历所有岗位
|
||||||
|
3. 对每个岗位进行完整匹配计算
|
||||||
|
4. 排序返回
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
- 需要传输1万个岗位的完整数据(几十MB)
|
||||||
|
- 需要计算所有岗位(即使明显不符合)
|
||||||
|
- 无法利用数据库索引优化
|
||||||
|
|
||||||
|
### ✅ 优化方案(快速实现)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 岗位数据存储在数据库中
|
||||||
|
2. 使用SQL WHERE条件快速过滤
|
||||||
|
3. 只对通过初步筛选的岗位进行详细计算
|
||||||
|
4. 排序返回
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 利用数据库索引,查询速度极快(毫秒级)
|
||||||
|
- 大幅减少需要计算的岗位数量(从1万降到几百)
|
||||||
|
- 只传输少量数据
|
||||||
|
|
||||||
|
## 具体实现步骤
|
||||||
|
|
||||||
|
### 第一步:数据库表结构设计
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 岗位表
|
||||||
|
CREATE TABLE positions (
|
||||||
|
id INT PRIMARY KEY,
|
||||||
|
-- 基础信息
|
||||||
|
position_name VARCHAR(255),
|
||||||
|
company_name VARCHAR(255),
|
||||||
|
|
||||||
|
-- 硬性条件(建立索引)
|
||||||
|
education_require VARCHAR(50), -- 学历要求:本科、硕士等
|
||||||
|
degree_require VARCHAR(50), -- 学位要求:学士、硕士等
|
||||||
|
age_min INT, -- 最小年龄
|
||||||
|
age_max INT, -- 最大年龄
|
||||||
|
gender_require VARCHAR(10), -- 性别要求:男、女、不限制
|
||||||
|
major_require TEXT, -- 专业要求
|
||||||
|
|
||||||
|
-- 其他信息
|
||||||
|
position_require JSON, -- 完整要求(JSON格式)
|
||||||
|
created_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 建立索引
|
||||||
|
CREATE INDEX idx_education ON positions(education_require);
|
||||||
|
CREATE INDEX idx_age ON positions(age_min, age_max);
|
||||||
|
CREATE INDEX idx_gender ON positions(gender_require);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第二步:快速过滤SQL查询
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 根据简历信息,快速过滤岗位
|
||||||
|
$resume = [
|
||||||
|
'birth_date' => '1995-03-01', // 计算年龄:30岁
|
||||||
|
'gender' => '男',
|
||||||
|
'education_level' => '硕士研究生',
|
||||||
|
'degree' => '硕士',
|
||||||
|
'major' => '计算机科学与技术'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算年龄
|
||||||
|
$age = calculateAge($resume['birth_date']); // 30岁
|
||||||
|
|
||||||
|
// SQL快速过滤(利用索引,毫秒级响应)
|
||||||
|
$positions = DB::table('positions')
|
||||||
|
->where(function($query) use ($resume, $age) {
|
||||||
|
// 学历要求:简历学历 >= 岗位要求
|
||||||
|
$query->whereIn('education_require', [
|
||||||
|
'本科', '本科及以上', '硕士', '硕士及以上'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 年龄要求:在范围内
|
||||||
|
$query->where('age_min', '<=', $age)
|
||||||
|
->where('age_max', '>=', $age);
|
||||||
|
|
||||||
|
// 性别要求:不限制 或 匹配
|
||||||
|
$query->where(function($q) use ($resume) {
|
||||||
|
$q->where('gender_require', '不限制')
|
||||||
|
->orWhere('gender_require', $resume['gender']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 专业要求:模糊匹配(或使用专业分类表)
|
||||||
|
$query->where('major_require', 'like', '%计算机%')
|
||||||
|
->orWhere('major_require', 'like', '%软件%');
|
||||||
|
})
|
||||||
|
->get(); // 可能从1万个筛选到200个
|
||||||
|
|
||||||
|
// 只对这200个岗位进行详细匹配计算
|
||||||
|
foreach ($positions as $position) {
|
||||||
|
$score = calculateMatchScore($position, $resume);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第三步:性能对比
|
||||||
|
|
||||||
|
| 方案 | 需要计算的岗位数 | 计算时间 | 数据库查询时间 |
|
||||||
|
|------|----------------|---------|---------------|
|
||||||
|
| **当前方案** | 10,000个 | 10-50秒 | 0秒(无数据库) |
|
||||||
|
| **优化方案** | 200个(过滤后) | 0.2-1秒 | 0.01-0.1秒 |
|
||||||
|
|
||||||
|
**总时间对比**:
|
||||||
|
- 当前方案:10-50秒
|
||||||
|
- 优化方案:0.21-1.1秒(**快50-200倍**)
|
||||||
|
|
||||||
|
## 关键技术点
|
||||||
|
|
||||||
|
### 1. 数据库索引优化
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 对常用查询字段建立索引
|
||||||
|
CREATE INDEX idx_education_age ON positions(education_require, age_min, age_max);
|
||||||
|
CREATE INDEX idx_gender ON positions(gender_require);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据预处理
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 岗位入库时,解析并存储结构化数据
|
||||||
|
$position = [
|
||||||
|
'position_require' => [
|
||||||
|
'学历要求' => '本科及以上',
|
||||||
|
'年龄要求' => '18周岁以上、35周岁以下',
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 解析并存储到独立字段
|
||||||
|
$position['education_require'] = '本科'; // 标准化
|
||||||
|
$position['age_min'] = 18;
|
||||||
|
$position['age_max'] = 35;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 专业匹配优化
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 方案A:使用专业分类表
|
||||||
|
CREATE TABLE major_categories (
|
||||||
|
major_name VARCHAR(100),
|
||||||
|
category VARCHAR(100),
|
||||||
|
INDEX idx_category(category)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 方案B:使用全文索引
|
||||||
|
ALTER TABLE positions ADD FULLTEXT INDEX idx_major(major_require);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 缓存机制
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 缓存常见简历的匹配结果
|
||||||
|
$cacheKey = "match:resume:{$resumeId}";
|
||||||
|
$result = Redis::get($cacheKey);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
// 计算并缓存
|
||||||
|
$result = calculateMatch($positions, $resume);
|
||||||
|
Redis::setex($cacheKey, 3600, $result); // 缓存1小时
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整实现流程
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function fastBatchMatch($resume, $page = 1, $pageSize = 20) {
|
||||||
|
// 1. 解析简历信息
|
||||||
|
$age = calculateAge($resume['birth_date']);
|
||||||
|
$education = parseEducation($resume['education']);
|
||||||
|
|
||||||
|
// 2. 数据库快速过滤(毫秒级)
|
||||||
|
$filteredPositions = DB::table('positions')
|
||||||
|
->where('education_require', '<=', $education['level'])
|
||||||
|
->where('age_min', '<=', $age)
|
||||||
|
->where('age_max', '>=', $age)
|
||||||
|
->where(function($q) use ($resume) {
|
||||||
|
$q->where('gender_require', '不限制')
|
||||||
|
->orWhere('gender_require', $resume['gender']);
|
||||||
|
})
|
||||||
|
->get(); // 从1万筛选到几百个
|
||||||
|
|
||||||
|
// 3. 只计算通过初步筛选的岗位(秒级)
|
||||||
|
$results = [];
|
||||||
|
foreach ($filteredPositions as $position) {
|
||||||
|
$score = $this->calculateMatchScore($position, $resume);
|
||||||
|
$results[] = [
|
||||||
|
'position_id' => $position->id,
|
||||||
|
'match_score' => $score,
|
||||||
|
'position' => $position
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 排序+分页
|
||||||
|
usort($results, fn($a, $b) => $b['match_score'] - $a['match_score']);
|
||||||
|
$paginated = array_slice($results, ($page - 1) * $pageSize, $pageSize);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'list' => $paginated,
|
||||||
|
'total' => count($results),
|
||||||
|
'page' => $page,
|
||||||
|
'page_size' => $pageSize
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
**其他机构能快速返回的关键**:
|
||||||
|
1. ✅ **岗位存储在数据库**(不是JSON数组)
|
||||||
|
2. ✅ **使用SQL WHERE快速过滤**(利用索引,毫秒级)
|
||||||
|
3. ✅ **只计算通过初步筛选的岗位**(从1万降到几百)
|
||||||
|
4. ✅ **数据预处理**(结构化存储硬性条件)
|
||||||
|
5. ✅ **索引优化**(对常用查询字段建索引)
|
||||||
|
|
||||||
|
**性能提升**:
|
||||||
|
- 数据库过滤:1万个 → 200个(减少98%)
|
||||||
|
- 计算时间:50秒 → 1秒(快50倍)
|
||||||
|
- **总响应时间:从50秒降到1秒以内**
|
||||||
|
|
||||||
|
## 实施建议
|
||||||
|
|
||||||
|
1. **如果岗位数据在数据库**:直接使用SQL过滤
|
||||||
|
2. **如果岗位数据是JSON**:考虑导入数据库或建立索引
|
||||||
|
3. **如果无法改数据库**:使用内存索引(如Elasticsearch)或预计算
|
||||||
|
|
||||||
|
|
||||||
161
doc/批量匹配接口说明.md
Normal file
161
doc/批量匹配接口说明.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# 批量匹配查询接口说明
|
||||||
|
|
||||||
|
## 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /match/batch
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接口说明
|
||||||
|
|
||||||
|
该接口基于数据库实现快速批量匹配查询,传入用户ID,自动从数据库读取用户简历信息和所有岗位,进行匹配计算并按匹配度排序返回。
|
||||||
|
|
||||||
|
## 请求参数
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
- **方法**: POST
|
||||||
|
- **Content-Type**: application/json 或 application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
### 请求参数说明
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| user_id | int | 是 | 用户ID |
|
||||||
|
| page | int | 否 | 页码,从1开始,默认1 |
|
||||||
|
| page_size | int | 否 | 每页数量,默认20,最大100 |
|
||||||
|
| filter_zero | bool | 否 | 是否过滤0分岗位,默认false |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
#### JSON格式请求
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": 527,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"filter_zero": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 表单格式请求
|
||||||
|
|
||||||
|
```
|
||||||
|
user_id=527&page=1&page_size=20&filter_zero=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应参数
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "查询成功",
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"position_id": 2,
|
||||||
|
"match_score": 95,
|
||||||
|
"position": {
|
||||||
|
"id": 2,
|
||||||
|
"positon_name": "人工智能与大数据侦察职位二",
|
||||||
|
"education_require": "本科及以上",
|
||||||
|
"degree_require": "学士及以上",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position_id": 1,
|
||||||
|
"match_score": 80,
|
||||||
|
"position": { ... }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 150,
|
||||||
|
"total_pages": 8,
|
||||||
|
"has_more": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"msg": "参数错误:user_id不能为空且必须为数字",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
### 1. 自动读取用户简历
|
||||||
|
|
||||||
|
接口会自动从数据库读取:
|
||||||
|
- 用户基本信息(`t_user`表)
|
||||||
|
- 教育经历(自动查找教育经历表)
|
||||||
|
|
||||||
|
### 2. 数据库快速过滤
|
||||||
|
|
||||||
|
使用SQL WHERE条件快速过滤岗位:
|
||||||
|
- 学历要求过滤
|
||||||
|
- 性别要求过滤
|
||||||
|
- 排除已删除的岗位
|
||||||
|
|
||||||
|
### 3. 匹配度计算
|
||||||
|
|
||||||
|
对通过初步筛选的岗位进行详细匹配计算:
|
||||||
|
- 硬性条件检查(一票否决)
|
||||||
|
- 软性条件评分(100分制)
|
||||||
|
|
||||||
|
### 4. 排序和分页
|
||||||
|
|
||||||
|
- 按匹配度降序排序
|
||||||
|
- 支持分页返回
|
||||||
|
- 可选过滤0分岗位
|
||||||
|
|
||||||
|
## 性能说明
|
||||||
|
|
||||||
|
- **数据库过滤**:利用SQL索引快速筛选,从1万个岗位可能筛选到几百个
|
||||||
|
- **计算时间**:只计算通过初步筛选的岗位,大幅减少计算量
|
||||||
|
- **响应时间**:通常在1-3秒内返回结果
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **教育经历表**:系统会自动查找以下表名:
|
||||||
|
- `t_user_education`
|
||||||
|
- `user_education`
|
||||||
|
- `t_education`
|
||||||
|
- `education`
|
||||||
|
|
||||||
|
如果您的教育经历表名不同,需要修改 `getUserResumeFromDb` 方法中的表名列表。
|
||||||
|
|
||||||
|
2. **年龄过滤**:由于年龄要求是文本格式(如"18周岁以上、35周岁以下"),难以用SQL精确匹配,会在详细匹配时检查。如需优化性能,建议在数据库中添加 `age_min` 和 `age_max` 字段。
|
||||||
|
|
||||||
|
3. **专业要求**:专业要求存储在 `position_other_require` 的JSON字段中,会在匹配时解析。
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查询用户ID为527的匹配岗位,第1页,每页20条
|
||||||
|
curl -X POST http://your-domain.com/match/batch \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"user_id": 527,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. **添加年龄字段**:在 `no_notice_position` 表中添加 `age_min` 和 `age_max` 字段,提升过滤效率
|
||||||
|
2. **建立索引**:对 `education_require`、`sex_require` 等字段建立索引
|
||||||
|
3. **缓存机制**:如果简历不变,可以缓存匹配结果
|
||||||
|
4. **专业分类表**:建立专业分类映射表,优化专业匹配
|
||||||
|
|
||||||
169
doc/批量匹配方案设计.md
Normal file
169
doc/批量匹配方案设计.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# 批量岗位匹配方案设计
|
||||||
|
|
||||||
|
## 需求分析
|
||||||
|
|
||||||
|
- **场景**:1万个岗位与1个简历进行匹配
|
||||||
|
- **要求**:按匹配度排序,支持分页返回
|
||||||
|
- **性能**:需要考虑内存和计算效率
|
||||||
|
|
||||||
|
## 方案设计
|
||||||
|
|
||||||
|
### 方案一:全量计算+排序+分页(推荐)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 实现简单
|
||||||
|
- 排序准确(需要全部计算才能正确排序)
|
||||||
|
- 支持分页
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 需要计算所有岗位(即使只返回一页)
|
||||||
|
- 内存占用较大(1万个岗位约几MB)
|
||||||
|
|
||||||
|
**适用场景**:岗位数量 < 5万,单次请求可接受
|
||||||
|
|
||||||
|
**实现思路**:
|
||||||
|
1. 遍历所有岗位,计算匹配度
|
||||||
|
2. 按匹配度降序排序
|
||||||
|
3. 根据分页参数截取结果
|
||||||
|
4. 返回分页数据
|
||||||
|
|
||||||
|
### 方案二:分批计算+分页(优化版)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 内存占用可控
|
||||||
|
- 可以提前终止(如果只需要前N个)
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 实现复杂
|
||||||
|
- 需要全部计算才能准确排序
|
||||||
|
|
||||||
|
**适用场景**:岗位数量 > 5万,内存受限
|
||||||
|
|
||||||
|
**实现思路**:
|
||||||
|
1. 分批处理岗位(如每批1000个)
|
||||||
|
2. 每批计算后合并排序
|
||||||
|
3. 分页返回
|
||||||
|
|
||||||
|
### 方案三:快速过滤+精确计算(最佳性能)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 性能最优
|
||||||
|
- 可以快速过滤掉0分岗位
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 需要两次遍历(先过滤,再计算)
|
||||||
|
|
||||||
|
**适用场景**:大部分岗位匹配度为0的情况
|
||||||
|
|
||||||
|
**实现思路**:
|
||||||
|
1. 第一遍:快速检查硬性条件,过滤掉明显不匹配的
|
||||||
|
2. 第二遍:对通过硬性条件的岗位进行详细计算
|
||||||
|
3. 排序+分页
|
||||||
|
|
||||||
|
## 推荐实现:方案一(全量计算+排序+分页)
|
||||||
|
|
||||||
|
### API设计
|
||||||
|
|
||||||
|
**接口地址**:`POST /match/batch`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"positions": [
|
||||||
|
{ "id": 1, "position_require": {...} },
|
||||||
|
{ "id": 2, "position_require": {...} }
|
||||||
|
],
|
||||||
|
"resume": { ... },
|
||||||
|
"page": 1, // 页码,从1开始
|
||||||
|
"page_size": 20, // 每页数量,默认20
|
||||||
|
"filter_zero": false // 是否过滤0分岗位,默认false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应格式**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"position_id": 2,
|
||||||
|
"match_score": 95,
|
||||||
|
"position": { ... } // 可选,是否返回完整岗位信息
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 10000,
|
||||||
|
"total_pages": 500,
|
||||||
|
"has_more": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能估算
|
||||||
|
|
||||||
|
**1万个岗位的计算时间**:
|
||||||
|
- 单个岗位匹配计算:约 1-5ms
|
||||||
|
- 1万个岗位:约 10-50秒(串行)
|
||||||
|
- 优化后(减少重复计算):约 5-25秒
|
||||||
|
|
||||||
|
**内存占用**:
|
||||||
|
- 单个岗位数据:约 1-5KB
|
||||||
|
- 1万个岗位:约 10-50MB
|
||||||
|
- 匹配结果:约 5-10MB
|
||||||
|
- 总计:约 15-60MB(可接受)
|
||||||
|
|
||||||
|
### 优化建议
|
||||||
|
|
||||||
|
1. **可选:过滤0分岗位**
|
||||||
|
- 如果 `filter_zero=true`,只返回匹配度>0的岗位
|
||||||
|
- 可以减少返回数据量
|
||||||
|
|
||||||
|
2. **可选:限制岗位数量**
|
||||||
|
- 如果岗位数量过大,可以限制最大处理数量(如最多1万个)
|
||||||
|
|
||||||
|
3. **可选:异步处理**
|
||||||
|
- 如果计算时间过长,可以考虑异步处理
|
||||||
|
- 使用队列+轮询的方式
|
||||||
|
|
||||||
|
4. **可选:缓存机制**
|
||||||
|
- 如果简历不变,可以缓存匹配结果
|
||||||
|
- 使用 Redis 缓存(key: resume_id, value: 匹配结果)
|
||||||
|
|
||||||
|
## 实现代码结构
|
||||||
|
|
||||||
|
```php
|
||||||
|
// MatchService.php
|
||||||
|
public function batchMatch(
|
||||||
|
array $positions,
|
||||||
|
array $resume,
|
||||||
|
int $page = 1,
|
||||||
|
int $pageSize = 20,
|
||||||
|
bool $filterZero = false
|
||||||
|
): array {
|
||||||
|
// 1. 计算所有岗位的匹配度
|
||||||
|
// 2. 排序
|
||||||
|
// 3. 过滤(可选)
|
||||||
|
// 4. 分页
|
||||||
|
// 5. 返回结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 控制器中调用
|
||||||
|
$matchService = new MatchService();
|
||||||
|
$result = $matchService->batchMatch(
|
||||||
|
$positions, // 1万个岗位
|
||||||
|
$resume, // 简历
|
||||||
|
1, // 第1页
|
||||||
|
20, // 每页20条
|
||||||
|
false // 不过滤0分
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
75
doc/数据库查询实现说明.md
Normal file
75
doc/数据库查询实现说明.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# 数据库查询实现说明
|
||||||
|
|
||||||
|
## 需要的信息
|
||||||
|
|
||||||
|
为了实现基于数据库的快速匹配查询功能,我需要了解以下信息:
|
||||||
|
|
||||||
|
### 1. 岗位表(positions/jobs)结构
|
||||||
|
|
||||||
|
请提供:
|
||||||
|
- 表名
|
||||||
|
- 主要字段(特别是硬性条件相关字段):
|
||||||
|
- 学历要求字段名和存储格式
|
||||||
|
- 学位要求字段名和存储格式
|
||||||
|
- 年龄要求字段名和存储格式(是范围还是文本描述)
|
||||||
|
- 性别要求字段名和存储格式
|
||||||
|
- 专业要求字段名和存储格式
|
||||||
|
- 其他要求字段
|
||||||
|
|
||||||
|
### 2. 简历表(resumes/users)结构(如果需要从数据库读取)
|
||||||
|
|
||||||
|
或者告诉我:
|
||||||
|
- 简历数据是通过接口参数传入,还是需要从数据库查询?
|
||||||
|
- 如果从数据库查询,请提供表结构和字段信息
|
||||||
|
|
||||||
|
### 3. 数据格式示例
|
||||||
|
|
||||||
|
请提供:
|
||||||
|
- 岗位表中学历要求的实际存储值(如:"本科"、"本科及以上"、"3"等)
|
||||||
|
- 年龄要求的存储格式(如:`age_min=18, age_max=35` 或 `"18周岁以上、35周岁以下"`)
|
||||||
|
- 专业要求的存储格式
|
||||||
|
|
||||||
|
## 实现方案
|
||||||
|
|
||||||
|
根据您提供的数据库结构,我将实现:
|
||||||
|
|
||||||
|
1. **快速过滤查询**:使用SQL WHERE条件快速筛选符合条件的岗位
|
||||||
|
2. **匹配度计算**:对筛选后的岗位进行详细匹配计算
|
||||||
|
3. **排序分页**:按匹配度排序并分页返回
|
||||||
|
|
||||||
|
## 请提供的信息格式
|
||||||
|
|
||||||
|
可以以以下任一方式提供:
|
||||||
|
|
||||||
|
### 方式一:SQL建表语句
|
||||||
|
```sql
|
||||||
|
CREATE TABLE positions (
|
||||||
|
id INT,
|
||||||
|
education_require VARCHAR(50),
|
||||||
|
...
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:表结构描述
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"table_name": "positions",
|
||||||
|
"fields": {
|
||||||
|
"id": "INT PRIMARY KEY",
|
||||||
|
"education_require": "VARCHAR(50) - 学历要求",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式三:示例数据
|
||||||
|
提供几条实际的岗位数据示例,我可以根据数据格式推断表结构。
|
||||||
|
|
||||||
|
## 实现后的功能
|
||||||
|
|
||||||
|
- ✅ 接口:`POST /match/batch` - 批量匹配查询
|
||||||
|
- ✅ 支持分页:`page`, `page_size` 参数
|
||||||
|
- ✅ 支持过滤0分:`filter_zero` 参数
|
||||||
|
- ✅ 快速响应:利用数据库索引,秒级返回结果
|
||||||
|
|
||||||
|
|
||||||
@@ -17,4 +17,7 @@ Route::get('think', function () {
|
|||||||
Route::get('hello/:name', 'index/hello');
|
Route::get('hello/:name', 'index/hello');
|
||||||
|
|
||||||
// 岗位简历匹配度计算接口
|
// 岗位简历匹配度计算接口
|
||||||
Route::post('match/calculate', 'match/calculate');
|
Route::post('match/calculate', '\app\controller\MatchController@calculate');
|
||||||
|
|
||||||
|
// 批量匹配查询接口(基于数据库)
|
||||||
|
Route::post('match/batch', '\app\controller\MatchController@batchMatch');
|
||||||
|
|||||||
40
test_debug.php
Normal file
40
test_debug.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
// 调试专业匹配
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$position = [
|
||||||
|
"position_require" => [
|
||||||
|
"专业-硕士" => "教育学、心理学"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$resume = [
|
||||||
|
"education" => [
|
||||||
|
[
|
||||||
|
"education_level" => "硕士研究生",
|
||||||
|
"majors_name" => "教育学"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$app = new think\App();
|
||||||
|
$app->initialize();
|
||||||
|
|
||||||
|
$matchService = new app\service\MatchService();
|
||||||
|
|
||||||
|
// 使用反射调用私有方法进行测试
|
||||||
|
$reflection = new ReflectionClass($matchService);
|
||||||
|
|
||||||
|
// 测试getMajorRequirement
|
||||||
|
$getMajorRequirement = $reflection->getMethod('getMajorRequirement');
|
||||||
|
$getMajorRequirement->setAccessible(true);
|
||||||
|
$majorRequire = $getMajorRequirement->invoke($matchService, $position['position_require'], $resume);
|
||||||
|
echo "专业要求: " . $majorRequire . "\n";
|
||||||
|
|
||||||
|
// 测试isMajorCategoryMatch
|
||||||
|
$isMajorCategoryMatch = $reflection->getMethod('isMajorCategoryMatch');
|
||||||
|
$isMajorCategoryMatch->setAccessible(true);
|
||||||
|
$result = $isMajorCategoryMatch->invoke($matchService, "教育学", "教育学、心理学");
|
||||||
|
echo "专业匹配结果: " . ($result ? "匹配" : "不匹配") . "\n";
|
||||||
|
|
||||||
78
test_new_case.php
Normal file
78
test_new_case.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
// 测试用户提供的新数据
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$position = [
|
||||||
|
"position_require" => [
|
||||||
|
"学历要求" => "本科",
|
||||||
|
"学位要求" => "学士学位",
|
||||||
|
"年龄要求" => "无",
|
||||||
|
"性别要求" => "不限制",
|
||||||
|
"民族要求" => "无",
|
||||||
|
"政治面貌要求" => "无",
|
||||||
|
"专业-本科" => "教育学",
|
||||||
|
"专业-硕士" => "教育学、心理学"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$resume = [
|
||||||
|
"birth_date" => "1998-03-01",
|
||||||
|
"gender" => "男",
|
||||||
|
"work_experience" => "无基层工作年限",
|
||||||
|
"education" => [
|
||||||
|
[
|
||||||
|
"education_level" => "普通本科",
|
||||||
|
"degree" => "学士",
|
||||||
|
"majors_name" => "教育学"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"education_level" => "硕士研究生",
|
||||||
|
"degree" => "硕士",
|
||||||
|
"majors_name" => "伦理学"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 加载ThinkPHP框架
|
||||||
|
$app = new think\App();
|
||||||
|
$app->initialize();
|
||||||
|
|
||||||
|
// 创建匹配服务实例
|
||||||
|
$matchService = new app\service\MatchService();
|
||||||
|
|
||||||
|
// 计算匹配度
|
||||||
|
$score = $matchService->calculateMatchScore($position, $resume);
|
||||||
|
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "测试结果\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "匹配度分数: {$score}/100分\n";
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// 检查硬性条件
|
||||||
|
echo "岗位要求:\n";
|
||||||
|
echo "- 学历要求: " . $position['position_require']['学历要求'] . "\n";
|
||||||
|
echo "- 学位要求: " . $position['position_require']['学位要求'] . "\n";
|
||||||
|
echo "- 年龄要求: " . $position['position_require']['年龄要求'] . "\n";
|
||||||
|
echo "- 性别要求: " . $position['position_require']['性别要求'] . "\n";
|
||||||
|
echo "- 专业-本科: " . $position['position_require']['专业-本科'] . "\n";
|
||||||
|
echo "- 专业-硕士: " . $position['position_require']['专业-硕士'] . "\n";
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
echo "简历信息:\n";
|
||||||
|
echo "- 最高学历: " . $resume['education'][1]['education_level'] . "\n";
|
||||||
|
echo "- 最高学位: " . $resume['education'][1]['degree'] . "\n";
|
||||||
|
echo "- 专业: ";
|
||||||
|
foreach ($resume['education'] as $edu) {
|
||||||
|
echo $edu['majors_name'] . " ";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
$age = date('Y') - date('Y', strtotime($resume['birth_date']));
|
||||||
|
if (date('md') < date('md', strtotime($resume['birth_date']))) {
|
||||||
|
$age--;
|
||||||
|
}
|
||||||
|
echo "- 年龄: {$age}岁\n";
|
||||||
|
echo "- 性别: " . $resume['gender'] . "\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
|
||||||
72
test_user_case.php
Normal file
72
test_user_case.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
// 测试用户提供的数据
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$position = [
|
||||||
|
"id" => 1,
|
||||||
|
"base_info" => [
|
||||||
|
"岗位名称" => "人工智能与大数据侦察职位一",
|
||||||
|
"招考单位" => "市级公安机关",
|
||||||
|
"招录人数" => "10",
|
||||||
|
"岗位代码" => "45150001"
|
||||||
|
],
|
||||||
|
"position_info" => [
|
||||||
|
"岗位名称" => "人工智能与大数据侦察职位一",
|
||||||
|
"招考单位" => "市级公安机关",
|
||||||
|
"招录人数" => "10",
|
||||||
|
"岗位代码" => "45150001"
|
||||||
|
],
|
||||||
|
"position_require" => [
|
||||||
|
"学历要求" => "本科及以上",
|
||||||
|
"学位要求" => "学士及以上",
|
||||||
|
"年龄要求" => "18周岁以上、35周岁以下。",
|
||||||
|
"专业(学科)类别" => "计算机科学与技术类,电气、电子及自动化类",
|
||||||
|
"其他资格条件" => "适合男性。符合人民警察录用条件。",
|
||||||
|
"专业资格条件" => "曾参加人工智能、大数据、计算机领域竞赛,获个人三等奖或团体三等奖及以上。"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$resume = [
|
||||||
|
"user_id" => 527,
|
||||||
|
"birth_date" => "1995-03-01",
|
||||||
|
"gender" => "男",
|
||||||
|
"work_experience" => "3年基层工作年限",
|
||||||
|
"education" => [
|
||||||
|
[
|
||||||
|
"education_level" => "本科",
|
||||||
|
"degree" => "学士",
|
||||||
|
"majors_name" => "计算机科学与技术"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 加载ThinkPHP框架
|
||||||
|
$app = new think\App();
|
||||||
|
$app->initialize();
|
||||||
|
|
||||||
|
// 创建匹配服务实例
|
||||||
|
$matchService = new app\service\MatchService();
|
||||||
|
|
||||||
|
// 计算匹配度
|
||||||
|
$score = $matchService->calculateMatchScore($position, $resume);
|
||||||
|
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "测试结果\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "匹配度分数: {$score}/100分\n";
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// 检查硬性条件
|
||||||
|
echo "检查硬性条件:\n";
|
||||||
|
echo "- 学历: " . $resume['education'][0]['education_level'] . "\n";
|
||||||
|
echo "- 学位: " . $resume['education'][0]['degree'] . "\n";
|
||||||
|
echo "- 专业: " . $resume['education'][0]['majors_name'] . "\n";
|
||||||
|
$age = date('Y') - date('Y', strtotime($resume['birth_date']));
|
||||||
|
if (date('md') < date('md', strtotime($resume['birth_date']))) {
|
||||||
|
$age--;
|
||||||
|
}
|
||||||
|
echo "- 年龄: {$age}岁\n";
|
||||||
|
echo "- 性别: " . $resume['gender'] . "\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
|
||||||
Reference in New Issue
Block a user