最近在帮朋友搭建一个基于ThinkPHP的求职招聘平台,发现这个框架在开发这类业务系统时确实有不少优势。WeJob这类网站本质上是要解决求职者与用人单位之间的信息匹配问题,而ThinkPHP的模块化设计和丰富的扩展库正好能快速实现这个需求。
从技术角度看,这类平台需要处理的核心业务包括:职位发布与管理、简历投递与筛选、用户权限分级、智能匹配算法等。ThinkPHP的ORM特性让数据库操作变得非常直观,配合其内置的分页功能和缓存机制,能够轻松应对招聘网站常见的高并发查询场景。
选择ThinkPHP 6.x版本作为基础框架,主要考虑以下几点:
提示:ThinkPHP 5.1到6.x的升级改动较大,建议新项目直接采用6.x版本,避免后期迁移成本。
招聘网站的核心数据模型包括:
sql复制CREATE TABLE `jobs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`title` varchar(100) NOT NULL,
`salary_min` decimal(10,2) NOT NULL,
`salary_max` decimal(10,2) NOT NULL,
`location` varchar(255) NOT NULL,
`description` text NOT NULL,
`requirements` text NOT NULL,
`is_remote` tinyint(1) DEFAULT '0',
`status` tinyint(1) DEFAULT '1',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
采用Elasticsearch作为搜索引擎,解决LIKE查询的性能问题:
bash复制composer require elasticsearch/elasticsearch
php复制$params = [
'index' => 'jobs_index',
'body' => [
'mappings' => [
'properties' => [
'title' => ['type' => 'text', 'analyzer' => 'ik_max_word'],
'description' => ['type' => 'text', 'analyzer' => 'ik_max_word'],
'location' => ['type' => 'keyword'],
'salary_min' => ['type' => 'integer'],
'is_remote' => ['type' => 'boolean']
]
]
]
];
$response = $client->indices()->create($params);
php复制public function search(Request $request)
{
$params = [
'index' => 'jobs_index',
'body' => [
'query' => [
'bool' => [
'must' => [
['match' => ['title' => $request->input('keywords')]],
['range' => ['salary_min' => ['gte' => $request->input('min_salary')]]]
],
'filter' => [
['term' => ['location' => $request->input('location')]]
]
]
],
'sort' => [
'salary_min' => ['order' => 'desc']
]
]
];
$results = $this->esClient->search($params);
return json($results);
}
使用PHP的PDF解析库提取简历文本内容:
php复制$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile('resume.pdf');
$text = $pdf->getText();
// 提取关键信息
preg_match_all('/工作经验(.+?)年/', $text, $matches);
$experience = $matches[1][0] ?? 0;
职位匹配度计算逻辑:
php复制public function calculateMatchScore($resume, $job)
{
$score = 0;
// 技能关键词匹配
$resumeSkills = explode(',', $resume['skills']);
$jobSkills = explode(',', $job['required_skills']);
$matchedSkills = array_intersect($resumeSkills, $jobSkills);
$score += count($matchedSkills) * 10;
// 工作经验匹配
if ($resume['experience'] >= $job['min_experience']) {
$score += 30;
}
// 学历匹配
if ($resume['education'] >= $job['education']) {
$score += 20;
}
// 薪资期望匹配
if ($resume['expected_salary'] <= $job['salary_max']) {
$score += 20;
}
return $score;
}
使用Workerman实现HR与求职者的在线沟通:
bash复制composer require workerman/workerman
php复制$worker = new Worker('websocket://0.0.0.0:2346');
$worker->onConnect = function($connection) {
echo "New connection\n";
};
$worker->onMessage = function($connection, $data) {
$message = json_decode($data, true);
// 存储聊天记录
Db::name('chat_messages')->insert([
'from_user' => $message['from'],
'to_user' => $message['to'],
'content' => $message['content'],
'created_at' => date('Y-m-d H:i:s')
]);
// 广播消息
foreach ($worker->connections as $client) {
if ($client->userId == $message['to']) {
$client->send($data);
}
}
};
Worker::runAll();
采用多级缓存方案:
php复制// 获取热门职位
$hotJobs = Cache::store('redis')->remember('hot_jobs', 3600, function() {
return Db::name('jobs')
->where('status', 1)
->order('view_count', 'desc')
->limit(10)
->select();
});
php复制public function jobDetail($id)
{
$cacheKey = 'job_'.$id;
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
$job = Db::name('jobs')->find($id);
$html = $this->fetch('detail', ['job' => $job]);
Cache::set($cacheKey, $html, 1800); // 缓存30分钟
return $html;
}
php复制// 错误做法
$companies = Db::name('companies')->select();
foreach ($companies as $company) {
$jobs = Db::name('jobs')->where('company_id', $company->id)->select();
}
// 正确做法 - 使用with预加载
$companies = Db::name('companies')
->with(['jobs' => function($query) {
$query->where('status', 1);
}])
->select();
php复制// 传统分页在数据量大时性能差
$jobs = Db::name('jobs')->paginate(15);
// 优化方案 - 使用游标分页
$lastId = $request->input('last_id', 0);
$jobs = Db::name('jobs')
->where('id', '>', $lastId)
->order('id', 'asc')
->limit(15)
->select();
ThinkPHP的查询构造器已经提供了参数绑定,但需要注意:
php复制// 不安全做法
$keyword = $_GET['keyword'];
Db::name('jobs')->where("title LIKE '%$keyword%'")->select();
// 安全做法
Db::name('jobs')->where('title', 'like', "%{$keyword}%")->select();
对用户输入内容进行过滤:
php复制// 在控制器中
$data = $request->only(['title', 'description']);
$data['description'] = clean($data['description'], 'html');
// 在模板中自动转义
{{ $job.description|raw }} // 危险
{{ $job.description }} // 安全
简历上传的安全处理:
php复制public function uploadResume(Request $request)
{
$file = $request->file('resume');
// 验证文件类型
$ext = $file->extension();
if (!in_array($ext, ['pdf', 'doc', 'docx'])) {
return error('仅支持PDF、Word格式');
}
// 验证文件内容
$content = file_get_contents($file->getRealPath());
if (preg_match('/<\s*script/i', $content)) {
return error('文件包含可疑内容');
}
// 重命名存储
$saveName = md5(uniqid()).'.'.$ext;
$file->move('/uploads/resumes', $saveName);
return success(['path' => '/uploads/resumes/'.$saveName]);
}
推荐使用Docker Compose部署:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8000:8000"
depends_on:
- mysql
- redis
- elasticsearch
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: wejob
redis:
image: redis:alpine
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.10.1
environment:
- discovery.type=single-node
ulimits:
memlock:
soft: -1
hard: -1
使用ThinkPHP的命令行配合Crontab实现:
php复制namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
class SendInterviewReminder extends Command
{
protected function configure()
{
$this->setName('send:reminder')->setDescription('发送面试提醒');
}
protected function execute(Input $input, Output $output)
{
$interviews = Db::name('interviews')
->where('status', 'pending')
->where('scheduled_time', 'between', [date('Y-m-d H:i:s'), date('Y-m-d H:i:s', strtotime('+1 day'))])
->select();
foreach ($interviews as $interview) {
// 发送邮件或短信提醒
$this->sendNotification($interview);
$output->writeln("已发送提醒给: {$interview['candidate_name']}");
}
}
}
对应的Crontab配置:
bash复制0 9 * * * cd /path/to/project && php think send:reminder
php复制$validate = new Validate([
'title' => 'require|max:100',
'salary_min' => 'require|number',
'description' => 'require|min:50'
]);
if (!$validate->check($input)) {
return error($validate->getError());
}
php复制try {
$this->processResume($file);
} catch (\Exception $e) {
Log::error('简历处理失败', [
'file' => $file->getOriginalName(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
这个项目从技术选型到最终上线历时3个月,期间最大的收获是认识到招聘业务场景的特殊性——既要保证企业HR的使用效率,又要考虑求职者的体验流畅度。ThinkPHP的灵活性和丰富的扩展生态让我们能够快速迭代各种功能需求。