Django招聘平台智能筛选系统:规则打分与简历匹配实现
2026/9/16 3:59:29 网站建设 项目流程

这套Django招聘平台我断断续续做了两个完整的周末,从需求梳理、库表设计到筛选打分逻辑,最后把职位发布、简历投递、Admin后台,以及基于规则和权重的中文简历筛选引擎全部打通。整理好的源码我按编号45148归档,里面包含MySQL建表脚本、演示数据和完整的筛选算法模块,改一下数据库连接就能直接跑起来。这篇就专门讲这套系统里最核心的部分:智能筛选到底怎么设计、怎么实现、怎么避开那些看着正常实际坑人的细节。

写这篇主要面向两类人。一类是学完Django基础课,想找个有含金量练手项目的同学;另一类是毕业设计选了“招聘网站”、“求职平台”这类题目,但不想交一份纯增删改查作业的在校生。智能筛选听起来像是个高大上的算法题,实际上用规则加权重打分的方式,就能做成一个“推荐排序合理、HR能看懂逻辑、演示效果足够”的版本。下面按选型、建表、算法、实操、跑通源码、排坑这个顺序讲,尽量把源码里每个关键文件都对应着说明,方便你拿着代码对照看。

1. 项目需求拆解与技术选型

1.1 招聘平台的核心业务闭环

做任何系统之前,先把“业务闭环”理顺。一个招聘平台至少有三类角色:求职者、招聘方(企业端)、系统管理员。核心闭环是:招聘方发布职位,求职者上传简历、浏览岗位、发起投递,系统对投递的简历做筛选,把匹配度高的候选人排到前面,招聘方在后台看到排序结果并安排面试。

在这个闭环里,真正耗人力的环节是“挑选简历”。一个热门岗位收到的简历动辄几十上百份,靠HR一份份打开、通读、凭感觉打分,标准不统一,效率也低。所以系统里必须有一层“自动化筛选”:职位发布后,系统能根据职位要求对简历做初筛和排序。这就是“智能筛选系统”在整个项目里的定位——它不是取代HR,而是帮HR把范围缩小、把顺序排好,让HR把时间花在真正值得看的候选人身上。

1.2 智能筛选为什么选“规则+权重”,而不是机器学习

这里有个很多人一上来就想歪的问题:智能筛选能不能直接用机器学习模型?比如用BERT做语义匹配,或者训练一个分类器判断“此人合不合适”。

我的判断是:在这个体量下,不值得。

原因有三点。第一,数据集太小。招聘平台在开发阶段能拿到的简历样本非常有限,别说深度学习,连有监督的分类模型都很难训练出稳定效果。第二,解释性差。面试官会问你“这个岗位为什么把某人排第一”,如果你用模型,回答是“模型算出来0.87分”,这不能让招聘方信服。第三,规则引擎完全够用。招聘筛选本身就是“硬性条件 + 偏好条件”的组合:学历、工作年限、技能、薪资预期,这些字段都是结构化数据,用“条件过滤 + 加权打分”能覆盖绝大多数场景,而且每一条筛选结果都能说清楚理由。

所以这套系统的实现方案是:硬性条件直接过滤,软性条件加权打分。硬性条件不满足,直接排除;软性条件按命中情况累计分数,最后按总分排序。这样既稳定、又可解释,还方便后续扩展——以后真要上模型,打分结果还能作为特征喂给模型,前面的逻辑不会白做。

1.3 Django在同类项目里的选型优势

选题方向定下来之后,框架层面我几乎没有犹豫就选了Django,原因很实在。

第一,Django自带Admin后台。招聘平台的招聘方管理和平台管理天然需要后台界面,Django Admin开箱即用,稍微定制一下就能满足演示和实际维护需求,省下大量前端时间。第二,ORM好用。职位、简历、投递记录这些关系模型,用Django ORM表达很直观,而且跨数据库迁移方便。第三,认证体系现成。User模型、权限系统、Session机制都已经内置,对于做三方角色这种场景,基本上配置一下就能用。

另外一个非常实际的原因:这类项目在课程设计、毕业设计里的出现频率极高,Django生态里的参考资源多。用Django做,前期踩坑有地方查,后期演示有官方文档兜底。权衡下来,Django是这类管理系统最稳的选择。

2. 数据库设计与核心模型

2.1 用户体系与三方角色,怎么设计才能不冗余

招聘平台用到Django默认的User模型不够,因为没有角色字段和企业信息。我的设计思路是:不要轻易改User表,而是用OneToOneField扩展Profile

具体做法是,User表仍然负责用户名、密码、邮箱这些基础认证信息,单独建一个Profile表存角色字段和手机号等扩展信息。再往下,把求职者资料单独放Resume表,把企业单独放Company表,企业主账号通过OneToOne关联到User,这样既不会把公司信息塞进User表导致字段爆炸,也能保证一个企业账号管理一个企业主体。

这里需要注意一个细节:很多初学者喜欢直接给Django的User模型加字段,但一旦后续要做迁移,或者想复用第三方应用,改动默认认证表会非常麻烦。用Profile扩展的方式,所有扩展逻辑都集中在自己应用里,风险小,也更好维护。

2.2 职位、简历、投递记录,三张核心表怎么拆

筛选系统的核心数据载体是三张表:职位表(Job)、简历表(Resume)、投递记录表(JobApplication)。职位表存企业发布岗位的硬性要求,简历表存求职者的能力画像,投递记录表则是在两者之间建立关联,并保存筛选结果分数。

Job表的关键字段我给一下,这是源码里实际的模型定义:

# apps/jobs/models.py from django.db import models from django.contrib.auth.models import User class Company(models.Model): name = models.CharField(max_length=100, verbose_name='公司名称') industry = models.CharField(max_length=50, blank=True, verbose_name='所属行业') description = models.TextField(blank=True, verbose_name='公司简介') owner = models.OneToOneField(User, on_delete=models.CASCADE, related_name='company', verbose_name='企业账号') class Job(models.Model): STATUS_CHOICES = ((1, '招聘中'), (0, '已下线')) title = models.CharField(max_length=100, verbose_name='职位名称') company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='jobs', verbose_name='所属企业') salary_min = models.IntegerField(default=0, verbose_name='最低薪资(K)') salary_max = models.IntegerField(default=0, verbose_name='最高薪资(K)') education_required = models.CharField(max_length=10, default='本科', verbose_name='学历要求') experience_required = models.IntegerField(default=1, verbose_name='经验要求(年)') skills_required = models.CharField(max_length=200, blank=True, verbose_name='必会技能,多个用逗号分隔') description = models.TextField(verbose_name='职位描述') status = models.SmallIntegerField(choices=STATUS_CHOICES, default=1, verbose_name='状态') created_at = models.DateTimeField(auto_now_add=True, verbose_name='发布时间') def __str__(self): return f'{self.company.name} - {self.title}'

这里技能字段我用逗号分隔的字符串,而不是建一张多对多表,理由是演示项目不需要过度设计,而且逗号分隔在打分时直接split即可,逻辑最简单。但如果你是做生产级系统,建议技能单独建表,否则后续维护技能字典会痛苦。

Resume表设计上,最重要的是把和筛选强相关的字段提出来:学历、工作年限、技能、期望薪资、自我评价。这些字段是后面筛选算法的直接输入。

# apps/resumes/models.py from django.db import models from django.contrib.auth.models import User class Resume(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='resume', verbose_name='对应用户') name = models.CharField(max_length=50, verbose_name='姓名') education = models.CharField(max_length=10, default='本科', verbose_name='最高学历') work_years = models.IntegerField(default=0, verbose_name='工作年限') skills = models.CharField(max_length=200, blank=True, verbose_name='技能,多个用逗号分隔') expected_salary = models.IntegerField(default=0, verbose_name='期望薪资(K)') self_evaluation = models.TextField(blank=True, verbose_name='自我评价') created_at = models.DateTimeField(auto_now_add=True)

JobApplication表是三张表里最值得讲的一张。投递记录不能只是“谁投了哪个岗位”这么简单,它必须保存筛选结果,这样列表页、详情页才能直接按匹配度排序展示。

# apps/jobs/models.py class JobApplication(models.Model): job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='applications', verbose_name='应聘职位') resume = models.ForeignKey('resumes.Resume', on_delete=models.CASCADE, related_name='applications', verbose_name='候选人简历') match_score = models.IntegerField(default=0, verbose_name='匹配度得分') status = models.CharField(max_length=20, default='待筛选', verbose_name='筛选状态') created_at = models.DateTimeField(auto_now_add=True, verbose_name='投递时间')

match_score字段就是筛选算法的出口。所有候选人的分数最后都写在这里,页面拿到数据后直接order_by('-match_score'),简单可靠。

2.3 筛选规则的配置化思路

筛选规则做配置化,是我在完成核心功能后又补的一个设计。因为招聘平台的业务规则会变:今天要求本科以上,明天可能要求硕士;今天经验门槛是1年,明天可能是3年。如果把规则写死在脚本里,改一次就得动代码,不合理。

解决办法是建一张FilterRule表,把常用筛选条件的阈值放进去:

# apps/screening/models.py class FilterRule(models.Model): job = models.OneToOneField('jobs.Job', on_delete=models.CASCADE, related_name='filter_rule') min_education = models.CharField(max_length=10, default='本科') min_experience = models.IntegerField(default=1) required_skills = models.CharField(max_length=200, blank=True) min_salary_match = models.IntegerField(default=0, help_text='薪资偏差容忍度(K)') updated_at = models.DateTimeField(auto_now=True)

这样做的直接好处是:HR在Admin后台就能调整某个岗位的筛选门槛,不用改代码,也不会影响其他岗位。尤其是答辩演示的时候,现场改一下阈值、看排序结果变化,效果比念代码强太多。

3. 智能筛选核心算法怎么实现

3.1 硬性条件过滤:先保证不把明显不合适的人推给HR

筛选算法的第一步是硬性条件过滤。这一步的目的是“排除”,不是“排序”。学历不达标、工作年限不够、必会技能一个都不会,这几类候选人直接结束流程,不进入后续打分。

学历这块,我给学历等级做了一张映射表:

GRADE_MAP = {'大专': 1, '本科': 2, '硕士': 3, '博士': 4}

比较时拿简历学历的等级值和岗位要求学历的等级值直接比较。比如岗位要求本科,那么大专的简历就被过滤掉;岗位要求硕士,本科生也被过滤掉。工作年限是整数比较,岗位要求3年,简历写2年,直接排除,没得商量。

技能硬过滤是整个筛选里最容易出问题的地方。岗位要求“Python、Django、MySQL”,如果候选人技能全空,或者一个都不沾边,这个人投进来几乎不可能是有效候选人。这块过滤的时候要注意技能大小写问题,比如“python”和“Python”要统一转成小写再比较。

这里放一段核心判断代码,对照源码里screening/services.py看:

def check_hard_conditions(job, resume): """硬性条件过滤,返回(是否通过, 原因列表)""" reasons = [] if GRADE_MAP.get(resume.education, 0) < GRADE_MAP.get(job.education_required, 2): reasons.append(f'学历未达标:要求{job.education_required},简历为{resume.education}') if resume.work_years < job.experience_required: reasons.append(f'工作经验不足:要求{job.experience_required}年,简历为{resume.work_years}年') job_skills = [s.strip().lower() for s in job.skills_required.split(',') if s.strip()] resume_skills = [s.strip().lower() for s in resume.skills.split(',') if s.strip()] if job_skills and not set(job_skills) & set(resume_skills): reasons.append(f'未命中任何必会技能:{job.skills_required}') return len(reasons) == 0, reasons

如果硬性条件不通过,match_score直接为0,系统的理解是“简历还没资格进入评分环节”。这一步可以避免HR看到一堆歪瓜裂枣排在前面,是筛选体验的第一道防线。

3.2 技能匹配与关键词覆盖打分,细节全在这

通过硬性过滤之后,进入评分环节。评分主要由三个维度组成:技能匹配度、关键词覆盖度、薪资匹配度,最后再加学历加分。

技能匹配度是权重最大的部分。岗位发布时填了“必会技能”,简历里技能字段能命中多少,直接决定基础分。具体算法是:命中技能数除以岗位要求技能总数,得到命中率,然后乘以50分。比如岗位要求4个技能,简历命中2个,技能分就是25分。

这里有个经验:不要用“全命中才给满分”的策略,否则要求越多的岗位,候选人越难拿到高分,排序反而不合理。按比例给分,能更好地区分“部分匹配”和“完全没匹配”的候选人。

关键词覆盖度是第二个维度,逻辑是:从职位描述里提取关键词,看简历文本里能覆盖多少。职位描述通常很长,需要先分词、再去掉停用词。中文不像英文天然有空格分词,这套系统里用的是按标点和常见分隔符直接切分,再用停用词表过滤。停用词得根据招聘场景维护,比如“要求”、“负责”、“熟悉”、“优先”、“相关”、“能力”这些词对筛选没有区分度,必须去掉,否则得分会虚高。

import re STOPWORDS = {'要求', '负责', '熟悉', '优先', '相关', '能力', '岗位', '工作', '以上', '我们', '以及', '具有', '良好'} def extract_keywords(text): parts = re.split(r'[\s,,。、;;::()()]+', text) return [p for p in parts if len(p) >= 2 and p not in STOPWORDS]

关键词匹配的时候,简历文本要把“自我评价”和“技能”拼接起来作为匹配池。这里有个特别容易踩的坑:如果候选人没写自我评价,匹配池文本很短,关键词覆盖分就会偏低,但这不代表候选人一定差。所以空自我评价时要给保底分,避免所有没写自我评价的人都被排在最后。

3.3 综合匹配度计算:分数怎么归一化到100分

筛选算法最后输出的match_score是0到100的整数,为了让招聘方一眼看懂,分数越高代表越匹配。综合分的计算公式是:

  • 技能命中分:命中率 × 50
  • 关键词覆盖分:覆盖比例 × 20
  • 薪资匹配分:0到20分之间
  • 学历加分:满足岗位要求学历再加10分

薪资匹配这里我多说两句。判断标准不是简单的“候选人期望在岗位薪资区间内就给满分”,而是要容忍一定偏差。岗位给15K到25K,候选人期望20K,这是完美匹配;候选人期望28K,岗位最高25K,这人可能也有谈的空间,不能直接踢掉,给个中档分就行了。所以薪资匹配分按三档计算:

def calc_salary_score(job, resume): mid_salary = (job.salary_min + job.salary_max) / 2 if job.salary_min <= resume.expected_salary <= job.salary_max: return 20 if abs(resume.expected_salary - mid_salary) <= 5: return 12 return 5

单位是K,所以5代表5000块的偏差容忍度。

学历加分的设计意图是:硬性过滤里学历已经过了,为什么还要加分?因为两个候选人同样技能水平、同样关键词覆盖,硕士学历的候选人,在真实招聘里确实比本科学历更有竞争力,给10分加成能让这种人排得更靠前。

打分函数完整代码如下:

# apps/screening/services.py def calc_match_score(job, resume): passed, reasons = check_hard_conditions(job, resume) if not passed: return 0, reasons score = 0 # 技能匹配分,最高50 job_skills = [s.strip().lower() for s in job.skills_required.split(',') if s.strip()] resume_skills = [s.strip().lower() for s in resume.skills.split(',') if s.strip()] hit_skills = set(job_skills) & set(resume_skills) hit_rate = len(hit_skills) / len(job_skills) if job_skills else 1 score += round(hit_rate * 50) # 关键词覆盖分,最高20 keywords = extract_keywords(job.description) full_text = (resume.self_evaluation or '') + resume.skills hit_keywords = [k for k in keywords if k in full_text] if keywords: score += round(len(hit_keywords) / len(keywords) * 20) else: score += 10 # 薪资匹配分,最高20 score += calc_salary_score(job, resume) # 学历加分,最高10 if GRADE_MAP.get(resume.education, 0) >= GRADE_MAP.get(job.education_required, 2): score += 10 return score, reasons

最后批量筛选的函数也很简单,把所有简历过一遍这套打分逻辑,分数大于0的留下来,按分数降序排:

def batch_screen(job): results = [] target_resumes = Resume.objects.all().select_related('user') for resume in target_resumes: score, reasons = calc_match_score(job, resume) if score > 0: results.append({'resume': resume, 'score': score, 'reasons': reasons}) results.sort(key=lambda x: x['score'], reverse=True) return results

3.4 这套打分规则后续怎么扩展

很多人担心规则写死了,以后需求一变就得推翻重来。其实不会。这个打分模型的每个维度都是独立的函数,想加“所在地匹配”就加一个calc_location_score模块,想加“离职状态优先”就加一个状态权重项,再把权重在综合分计算里调一下就行。

甚至后面想引入机器学习,这套规则打分的结果本身就是一份现成的特征数据。把技能命中率、关键词覆盖率、薪资偏差、学历等级作为特征,把HR最终是否约面作为标签,就能训一个简单模型。所以这个方案的扩展性,比表面上看起来要强得多。

4. 核心功能模块开发与后台体验

4.1 招聘方的职位发布与管理模块实现

职位发布模块对招聘方来说是最基础的功能。登录后进入招聘方后台,能发布新职位、管理已发布职位、下线不再招聘的岗位。开发这个模块的时候,有两点值得说说。

第一,权限隔离。招聘方登录后,只允许看自己公司的职位,不能看到别的公司数据。这一点在Django里用QuerySet过滤实现:在ListView或者ORM查询里,加一个company=request.user.company的条件就行。千万别忘了加,否则就没法演示了。第二,表单校验。职位表单用ModelForm,Django自动根据模型字段生成校验逻辑,但我额外加了salary_max不得小于salary_min这种跨字段校验。

# apps/jobs/forms.py class JobForm(forms.ModelForm): class Meta: model = Job fields = ['title', 'salary_min', 'salary_max', 'education_required', 'experience_required', 'skills_required', 'description'] def clean(self): cleaned_data = super().clean() if cleaned_data.get('salary_max') < cleaned_data.get('salary_min'): raise forms.ValidationError('最高薪资不能低于最低薪资') return cleaned_data

4.2 投递后自动打分:简历名单是怎么生成的

智能筛选的结果不是独立按钮触发的,而是在候选人投递简历的瞬间,系统自动完成打分并写入投递记录。这样做的好处是,招聘方打开后台时,所有候选人都已经带好分数,不用等筛选跑完。

实现上,在投递流程里直接调用打分函数:

# apps/jobs/services.py def apply_job(job, resume): application, created = JobApplication.objects.get_or_create( job=job, resume=resume ) if created: application.match_score, _ = calc_match_score(job, resume) application.save() return application

很多项目会在这里用Django signal或者异步队列来做,但我的建议是:这个体量下同步调用就行。第一,打分逻辑是纯CPU计算,简历总量一百份以内的话,单次计算耗时基本可以忽略。第二,同步调用出问题好排查,直接在投递请求的日志里就能看到结果。异步反而会引入消息队列的复杂度,不值当。

4.3 筛选结果展示与可解释说明

分数计算出来之后,页面展示不能只是摆一个数字。我给候选人列表加了一列“推荐理由”,把calc_match_score返回的reasons列表渲染出来。比如:

  • 技能匹配:命中3/5项
  • 关键词覆盖:命中8/15个职位关键词
  • 薪资期望:在岗位区间内
  • 学历:硕士,满足要求

听上去不复杂,但对招聘方来说这个信息比一个冷冰冰的分数有用得多。HR能知道这个人为什么排前面,也能自己判断这个分数是否合理。这里也是答辩时最容易被问到的点——“你的系统不只是给个分数,还给理由”,这个设计在演示时非常加分。

列表页排序就是简单的一句order_by('-match_score'),再配一个筛选侧边栏,按学历、经验年限、薪资范围二次过滤。数据量上来之后记得在match_score加索引,否则ORDER BY会变慢。

4.4 给Django Admin加实用动作和界面优化

Admin后台在这套系统里承担着平台管理员和招聘方的日常管理功能。默认的Admin列表页信息太单调,而且不能在列表页做批量操作,体验不行。我做了两件比较重要的事。

第一,加actions。给JobAdmin注册了“批量下线岗位”和“对选中岗位执行智能筛选”两个动作。批量下线很好理解;执行智能筛选则是对选中岗位跑一遍batch_screen,然后把打分结果写到JobApplication表里。这为HR提供了一个手动触发筛选的入口,比如岗位要求改过之后,重新跑一次刷新候选人分数。

# apps/jobs/admin.py from django.contrib import admin from apps.screening.services import batch_screen from .models import Job @admin.register(Job) class JobAdmin(admin.ModelAdmin): list_display = ('title', 'company', 'salary_min', 'salary_max', 'education_required', 'status', 'created_at') list_filter = ('status', 'education_required', 'company') search_fields = ('title', 'description') list_per_page = 20 actions = ['make_offline', 're_screen'] @admin.action(description='批量下线岗位') def make_offline(self, request, queryset): updated = queryset.update(status=0) self.message_user(request, f'已下线 {updated} 个岗位') @admin.action(description='重新执行智能筛选') def re_screen(self, request, queryset): count = 0 for job in queryset: results = batch_screen(job) for item in results: JobApplication.objects.update_or_create( job=job, resume=item['resume'], defaults={'match_score': item['score']} ) count += 1 self.message_user(request, f'已更新 {count} 条筛选结果')

第二,list_display和list_filter配好之后,管理员在列表页就能一目了然地看到所有职位状态和薪资范围,不用点进详情页。Admin界面美化方面,Django自带的后台样式其实已经够用了,不用大改。如果想让颜色和Logo更贴合业务,在Admin的base_site.html模板里覆盖title和header就行。项目里我也放了简单示例,改了顶部标题,加了平台名称。

5. 源码结构、部署步骤与演示数据

5.1 源码目录结构,关键文件对应哪些功能

拿到“源码45148”压缩包之后,先别急着跑命令,花两分钟对照一下目录,搞清楚每个目录是干嘛的,后面改东西才不迷路。

django-job-filter/ ├── manage.py ├── requirements.txt ├── config/ # 项目总配置 │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── apps/ │ ├── users/ # 用户Profile扩展 │ ├── jobs/ # 职位、公司、投递记录 │ ├── resumes/ # 简历管理 │ └── screening/ # 智能筛选算法核心 │ ├── services.py # 打分引擎 │ └── models.py # FilterRule模型 ├── templates/ │ ├── base.html │ ├── jobs/ # 职位列表、详情、发布页 │ ├── resumes/ # 简历中心 │ └── screening/ # 筛选结果页 ├── static/ │ ├── css/ │ └── js/ ├── scripts/ │ ├── init_demo_data.py # 生成演示数据 │ └── create_admin.py # 创建超级管理员 └── docs/ └── README.md # 运行说明和账号信息

这套目录结构遵循了Django官方推荐的“多应用”模式,把不同业务拆成独立app。特别注意,screening这个app只放算法和规则相关的代码,不掺页面模板,这样以后算法升级可以独立测试,不影响其他模块。

5.2 从源码到跑通的完整步骤

代码跑通分四步,前提是你本机已经装好了Python 3.8以上版本。

第一步建虚拟环境、装依赖:

cd django-job-filter python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install -r requirements.txt

requirements.txt里面关键是这几个包:

Django==4.2.10 PyMySQL==1.1.0 python-decouple==3.8 django-widget-tweaks==1.5.0

注意我特意放了PyMySQL而不是mysqlclient,原因后面排坑部分细说。如果机房/本机没装MySQL,也可以先用SQLite顶上,把settings.py里的DATABASES配置换成默认的sqlite3就能跑,但不建议长期这么做,因为筛选SQL在两种数据库下性能表现不同。

第二步改数据库配置。在config/settings.py里找到DATABASES这一段,改成你自己的MySQL连接信息:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'job_filter_db', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }

记得先在MySQL里建好同名数据库:

CREATE DATABASE job_filter_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

第三步初始化数据库并创建管理员:

python manage.py makemigrations python manage.py migrate python scripts/create_admin.py

第四步导入演示数据并启动:

python scripts/init_demo_data.py python manage.py runserver

浏览器访问http://127.0.0.1:8000,用create_admin.py里输出的管理员账号登录Admin后台,或者用演示数据里的求职者/招聘方账号登录前端,就能看到预置好的职位、简历和筛选结果。

5.3 演示数据与测试账号,演示前先跑一遍

演示数据是我特意准备的,包含5家企业、8个岗位、20份简历,以及一批已经生成的投递记录和打分结果。这样一打开系统就是“有内容”的状态,不用自己手动造数据。

测试账号分三类:

角色用户名密码
平台管理员adminadmin123456
招聘方hr_company1hr123456
求职者candidate1cand123456

管理员登录Admin后台,能看到企业的筛选规则配置;招聘方登录前端,能看到自己公司职位收到的所有候选人以及按匹配度排序的名单;求职者登录后可以查看职位并发起投递,投递后回到招聘方后台,马上就能看到新候选人以及实时算出来的匹配分。

这套演示路径基本能覆盖整个业务闭环,面试官或导师让操作的时候,照着这个流程走就行。

6. 常见问题排坑与排查技巧实录

6.1 mysqlclient 装不上,是最常见的拦路虎

如果按某些教程在Windows上直接pip install mysqlclient,大概率会碰到一串编译错误。mysqlclient依赖系统底层库,Windows上经常会因为缺少MSVC编译环境而失败。解决方案有两个。

方案一:像我一样直接用PyMySQL,然后需要在项目的__init__.py里做一次猴子补丁,让Django的MySQL后端把PyMySQL当mysqlclient用:

# config/__init__.py import pymysql pymysql.install_as_MySQLdb()

方案二(Linux环境):先装系统依赖再装mysqlclient:

sudo apt-get install python3-dev default-libmysqlclient-dev build-essential pip install mysqlclient

在我实际带过的学员里,按方案一踩的坑明显更少。这是我在源码里直接选用PyMySQL的原因。

6.2 中文乱码和数据查不到,先检查字符集与编码

中文乱码这个问题,九成是字符集设置不对。数据库建的库不是utf8mb4,或者Django连接MySQL时没有指定charset,都可能插入中文字段时能看到,但查询出来是乱码,或者干脆插入报错。

数据库层面,建库语句一定要写:

CREATE DATABASE job_filter_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Django连接层面,就在DATABASES配置里加OPTIONS里的charset参数。另外还要确保settings.py里的LANGUAGE_CODE:

LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True

这里有个细节:USE_TZ设为True时,Django往MySQL里存的是UTC时间,页面上显示时要手动转本地时区。我在这套源码里的所有模板中,都用了Django的{% localtime %}标签或者在查询时加了timezone.localtime转换,否则演示的时候时间会差8小时,被细心的面试官问起来会很尴尬。

6.3 打分结果总是不准,先排查这四个点

我调试打分逻辑的时候,几乎把能犯的错都犯了一遍。如果你的筛选结果看起来“不太智能”,先别急着改算法,按下面四个点排查。

第一,技能分隔符不一致。有些简历技能字段里用的是中文逗号“,”,有些用的是英文逗号“,”,split的时候全用了英文逗号,中文逗号的那条记录技能就全部没匹配上。解决方案是分词时统一replace中文逗号为英文逗号。

第二,大小写问题。岗位要求“Python”,简历技能写“python”,直接字符串比较是false。匹配前全部转小写,这条我在完整代码里已经处理了,但自己改代码的时候非常容易漏掉。

第三,自我评价为空。简历没填自我评价,关键词覆盖分特别低,这人就永远排后面。所以我在源码里对self_evaluation或者self_evaluation为空的场景,做了保底处理:关键词维度给一个中等分,而不是让人垫底。

第四,薪资边界判断错误。期望薪资恰好等于岗位最低工资,或者恰好等于岗位最高工资,这种边界值一定要用小于等于和大于等于,不能只写小于或大于,否则边界处的真实有效简历会被误判。

这四个点排查完,打分准确率能提升一个档次。

6.4 Admin后台样式丢失和搜索变慢

Admin后台登录进去只有一个赤裸裸的HTML,没有CSS样式,这是生产模式下静态文件没生效。开发模式跑runserver基本不会遇到,但如果你用uwsgi或gunicorn部署,就需要先执行:

python manage.py collectstatic

并在settings.py里配好STATIC_ROOT和STATIC_URL。另外,Admin列表页数据多了之后,加上了search_fields和list_filter,查询会变慢,这是正常的,因为默认情况下这些字段没建索引。如果你真的拿到几千条职位数据在演示,建议给Job表的status和JobApplication表的match_score加索引,排序和过滤会快很多。

最后说点实在的

这套系统做完之后,我自己拿一批简历跑了模拟数据,最直观的感受是:硬过滤加打分排序,真的能把“完全不相关”的人压到列表底部,HR只需要从前20%的候选人里挑人,效率比纯人工翻高太多了。项目本身不算复杂,但“筛选逻辑可解释”这一点,是它跟普通增删改查系统拉开差距的地方。

源码里我把筛选引擎单独放在screening/services.py,所有跟算法相关的代码都和视图解耦了。后你想改造成前后端分离,或者加个看板统计,都可以直接复用这套逻辑。我个人的体会是,像“智能”这种词不一定非得用深度学习,把业务规则结构化、让系统替人做重复判断,本身就是很有价值的落地思路。

最后再分享一个小技巧:如果你打算拿这个项目去答辩,建议现场演示一次“修改岗位要求后重新筛选”的操作,让面试官看到候选人排序跟着规则实时变化,这个动作比任何口头解释都有说服力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询