
这套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_length100, verbose_name公司名称) industry models.CharField(max_length50, blankTrue, verbose_name所属行业) description models.TextField(blankTrue, verbose_name公司简介) owner models.OneToOneField(User, on_deletemodels.CASCADE, related_namecompany, verbose_name企业账号) class Job(models.Model): STATUS_CHOICES ((1, 招聘中), (0, 已下线)) title models.CharField(max_length100, verbose_name职位名称) company models.ForeignKey(Company, on_deletemodels.CASCADE, related_namejobs, verbose_name所属企业) salary_min models.IntegerField(default0, verbose_name最低薪资(K)) salary_max models.IntegerField(default0, verbose_name最高薪资(K)) education_required models.CharField(max_length10, default本科, verbose_name学历要求) experience_required models.IntegerField(default1, verbose_name经验要求(年)) skills_required models.CharField(max_length200, blankTrue, verbose_name必会技能多个用逗号分隔) description models.TextField(verbose_name职位描述) status models.SmallIntegerField(choicesSTATUS_CHOICES, default1, verbose_name状态) created_at models.DateTimeField(auto_now_addTrue, 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_deletemodels.CASCADE, related_nameresume, verbose_name对应用户) name models.CharField(max_length50, verbose_name姓名) education models.CharField(max_length10, default本科, verbose_name最高学历) work_years models.IntegerField(default0, verbose_name工作年限) skills models.CharField(max_length200, blankTrue, verbose_name技能多个用逗号分隔) expected_salary models.IntegerField(default0, verbose_name期望薪资(K)) self_evaluation models.TextField(blankTrue, verbose_name自我评价) created_at models.DateTimeField(auto_now_addTrue)JobApplication表是三张表里最值得讲的一张。投递记录不能只是“谁投了哪个岗位”这么简单它必须保存筛选结果这样列表页、详情页才能直接按匹配度排序展示。# apps/jobs/models.py class JobApplication(models.Model): job models.ForeignKey(Job, on_deletemodels.CASCADE, related_nameapplications, verbose_name应聘职位) resume models.ForeignKey(resumes.Resume, on_deletemodels.CASCADE, related_nameapplications, verbose_name候选人简历) match_score models.IntegerField(default0, verbose_name匹配度得分) status models.CharField(max_length20, default待筛选, verbose_name筛选状态) created_at models.DateTimeField(auto_now_addTrue, 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_deletemodels.CASCADE, related_namefilter_rule) min_education models.CharField(max_length10, default本科) min_experience models.IntegerField(default1) required_skills models.CharField(max_length200, blankTrue) min_salary_match models.IntegerField(default0, help_text薪资偏差容忍度(K)) updated_at models.DateTimeField(auto_nowTrue)这样做的直接好处是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(keylambda x: x[score], reverseTrue) return results3.4 这套打分规则后续怎么扩展很多人担心规则写死了以后需求一变就得推翻重来。其实不会。这个打分模型的每个维度都是独立的函数想加“所在地匹配”就加一个calc_location_score模块想加“离职状态优先”就加一个状态权重项再把权重在综合分计算里调一下就行。甚至后面想引入机器学习这套规则打分的结果本身就是一份现成的特征数据。把技能命中率、关键词覆盖率、薪资偏差、学历等级作为特征把HR最终是否约面作为标签就能训一个简单模型。所以这个方案的扩展性比表面上看起来要强得多。4. 核心功能模块开发与后台体验4.1 招聘方的职位发布与管理模块实现职位发布模块对招聘方来说是最基础的功能。登录后进入招聘方后台能发布新职位、管理已发布职位、下线不再招聘的岗位。开发这个模块的时候有两点值得说说。第一权限隔离。招聘方登录后只允许看自己公司的职位不能看到别的公司数据。这一点在Django里用QuerySet过滤实现在ListView或者ORM查询里加一个companyrequest.user.company的条件就行。千万别忘了加否则就没法演示了。第二表单校验。职位表单用ModelFormDjango自动根据模型字段生成校验逻辑但我额外加了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_data4.2 投递后自动打分简历名单是怎么生成的智能筛选的结果不是独立按钮触发的而是在候选人投递简历的瞬间系统自动完成打分并写入投递记录。这样做的好处是招聘方打开后台时所有候选人都已经带好分数不用等筛选跑完。实现上在投递流程里直接调用打分函数# apps/jobs/services.py def apply_job(job, resume): application, created JobApplication.objects.get_or_create( jobjob, resumeresume ) 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(status0) 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( jobjob, resumeitem[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.txtrequirements.txt里面关键是这几个包Django4.2.10 PyMySQL1.1.0 python-decouple3.8 django-widget-tweaks1.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环境先装系统依赖再装mysqlclientsudo 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_CODELANGUAGE_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所有跟算法相关的代码都和视图解耦了。后你想改造成前后端分离或者加个看板统计都可以直接复用这套逻辑。我个人的体会是像“智能”这种词不一定非得用深度学习把业务规则结构化、让系统替人做重复判断本身就是很有价值的落地思路。最后再分享一个小技巧如果你打算拿这个项目去答辩建议现场演示一次“修改岗位要求后重新筛选”的操作让面试官看到候选人排序跟着规则实时变化这个动作比任何口头解释都有说服力。