Django+Vue构建毕业生就业智能推荐系统:从数据建模到推荐算法落地
选“基于Django的毕业生就业智能推荐信息系统”当毕业设计第一反应可能觉得这就是个普通的管理系统无非是学生、企业、职位三个模块来回增删改查。但真正动手做完我的判断完全变了这个题目最有价值的地方恰恰是“智能推荐”四个字它要求你把学生画像、职位画像、匹配算法、前后端数据联动全部串起来。整套流程跑通之后你等于把一个标准的Web全栈项目从头到尾做了一遍从数据库设计到REST API再到Vue页面联调和推荐逻辑实现哪一环都躲不掉。这篇文章就写给正在纠结选题、或者已经选了类似题目但不知道怎么下手的同学。我会把整个项目的设计思路、数据库建模、推荐算法落地、前后端分离实施以及我实测踩过的坑全部摊开来讲。不管你最后是自己写还是参考别人的源码搞清楚这些核心逻辑答辩和写文档的时候你就不心虚。1. 选题怎么定为什么是“就业智能推荐”而不是传统管理系统1.1 这个题目到底在解决什么问题先想清楚一个问题市面上已经有很多招聘网站了一个毕业设计做就业系统意义在哪答案就在“智能匹配”这四个字上。传统的招聘系统本质是一个“信息发布平台”企业发职位、学生搜职位搜索靠的关键词匹配结果质量完全取决于学生会不会用关键词。比如一个计算机专业的学生他想找Java开发岗他可能搜“Java”也可能搜“后端”还可能搜“软件开发工程师”搜出来的结果五花八门还得自己逐条筛选效率很低。而智能推荐系统要解决的是另一件事在用户没有明确搜索意图的情况下系统根据学生自己的专业、技能、求职意向、学历水平主动把最匹配的职位推给他。这个逻辑听起来不难但一旦落实到系统设计上就意味着你需要定义“什么叫匹配”拆解成可计算的维度再用代码实现。所以这个题目的核心价值很清楚它不只是一个管理系统而是一个带算法逻辑的业务系统。对于毕设来说这个难度梯度刚刚好——比纯CRUD高出一截又不至于像论文级推荐系统那样需要你研究深度学习。1.2 技术选型Django加Vue前后端分离为什么是稳妥之选技术选型是我当时纠结最久的部分。备选方案其实不少我列个对比你们感受一下技术方案优势劣势Django Vue自带ORM、Admin后台、认证系统开发效率极高Python生态对数据处理友好性能不是最强但对毕设完全够用Spring Boot Vue企业级应用的主流方案面试聊起来有分量Java体系配置多学习成本高周期长Flask Vue轻量灵活适合小型项目很多功能要自己拼开发效率不如DjangoNode.js Express Vue前后端语言统一都是JavaScript后端生态相对薄弱重业务场景维护成本高我最后选了Django Vue这套组合基于三个理由。第一Django自带的后台管理功能太适合毕设了。你不需要自己写管理员界面Django Admin直接生成用来管理用户、职位、投递记录演示的时候非常直观。第二Django的ORM对新手极其友好。你不需要写SQL直接Python类定义模型迁移命令一键建表。我当时第一次用Django的Migrate功能时看着数据库表自动生成那种体验比手写SQL舒服太多。第三前后端分离架构本身就是一个加分项。毕业设计答辩时老师通常会问“你的项目有什么亮点”前后端分离、RESTful API设计、JWT认证这些都是可以主动铺开讲的点。前端我选了Vue3加Element Plus组件库成熟页面做出来颜值有保障而且Vue的学习曲线比React平缓对于主要精力在后端的同学来说更友好。1.3 项目的整体功能边界怎么划动手写代码之前一定要把功能边界划清楚不然后期会越做越乱。我按照角色把整个系统分成了三条线。学生端注册登录、完善个人档案包括专业、学历、技能标签、求职意向、期望薪资、期望地点、浏览推荐职位、主动搜索职位、投递简历、查看投递状态。企业端注册登录、维护企业信息、发布职位、管理职位上下架、查看收到的简历投递、更新投递状态待处理、已通过、已拒绝。管理员端利用Django Admin管理所有数据包括用户审核、职位审核、数据统计。这个边界划完之后我的开发顺序也就定了先搭项目框架再做用户认证再做基础档案管理最后做推荐引擎和前端的推荐页面。每一步都有明确产出不会出现做到一半不知道自己在干什么的情况。2. 数据模型设计与推荐算法落地2.1 数据建模学生、企业、职位三张核心表怎么设计数据模型是整个系统的地基。我的建议是不要一上来就建十几个表先抓住核心主线User、StudentProfile、CompanyProfile、JobPosition外加一个JobApplication投递记录表。有一个设计细节特别值得说不要把所有的字段堆在User表里。Django虽然允许你扩展User模型但我当时把学生和企业各自的扩展信息单独拆成了Profile表通过OneToOneField关联到User。这样做的原因很实际学生和企业的字段差异太大放在一张表里会有一堆空字段既不优雅后面写表单校验也很麻烦。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): ROLE_CHOICES ( (student, 学生), (company, 企业), ) role models.CharField(max_length20, choicesROLE_CHOICES, defaultstudent) phone models.CharField(max_length20, blankTrue, nullTrue) class StudentProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_namestudent_profile) school models.CharField(max_length100, verbose_name学校) major models.CharField(max_length100, verbose_name专业) education models.IntegerField(choices( (1, 专科), (2, 本科), (3, 硕士), (4, 博士) ), default2, verbose_name学历层次) skills models.JSONField(defaultlist, verbose_name技能标签) expected_city models.CharField(max_length50, verbose_name期望城市) expected_salary models.IntegerField(default0, verbose_name期望薪资) resume models.TextField(blankTrue, verbose_name简历简介) class JobPosition(models.Model): company models.ForeignKey(company.CompanyProfile, on_deletemodels.CASCADE, related_namejobs) title models.CharField(max_length100, verbose_name职位名称) industry models.CharField(max_length50, verbose_name行业领域) location models.CharField(max_length50, verbose_name工作地点) salary_min models.IntegerField(default0, verbose_name薪资下限) salary_max models.IntegerField(default0, verbose_name薪资上限) education_required models.IntegerField(choices( (1, 专科), (2, 本科), (3, 硕士), (4, 博士) ), default2, verbose_name学历要求) skills models.JSONField(defaultlist, verbose_name技能标签) description models.TextField(verbose_name职位描述) is_active models.BooleanField(defaultTrue, verbose_name是否上架) created_at models.DateTimeField(auto_now_addTrue)这里有两个细节可以讲给答辩老师听。第一个是学历字段的类型。学历我以前习惯用CharField存“本科”“硕士”这种字符串但后来做推荐匹配的时候发现字符串比较大小非常麻烦必须自己维护映射关系。改成IntegerField之后学历匹配就是一次简单的数值比较代码逻辑清晰得多。第二个是技能标签用JSONField存储。Django的JSONField可以直接存Python列表比如[Python, Django, MySQL]。虽然从严格的数据库范式角度来说这不是最优但考虑到技能标签的个数不固定、且查询时不需要对单个标签建立索引JSONField的灵活性和开发效率优势非常明显。2.2 推荐算法用加权评分把“智能”落到实处推荐算法是这类项目的灵魂。很多同学一听到“推荐算法”就发怵觉得必须用什么协同过滤、矩阵分解、深度学习。其实对于就业推荐这个场景这些高级算法未必是最优选。我当时做过一个权衡协同过滤依赖大量用户行为数据比如学生浏览了多少职位、投递了多少职位。但一个毕设项目里用户量撑死了几百个投递行为数据非常稀疏学出来的模型毫无意义而且还会遇到冷启动问题——一个新注册的学生没有任何行为记录协同过滤根本没法给他推荐。所以这个场景更适合基于内容的推荐核心思想就是计算学生画像和职位画像之间的相似度按相似度排序推荐。具体来说我从四个维度做了匹配评分匹配维度权重匹配规则学历匹配40%学生学历层次 职位要求学历得满分否则0分技能匹配30%计算学生技能与职位技能的交集重合度地点匹配20%学生期望城市与职位工作地点一致得满分薪资匹配10%学生期望薪资落在职位薪资区间内得满分这个权重分配是我根据就业推荐场景的经验调的学历和技能占主导地点和薪资作为调节项。每个维度的分数算完之后乘以权重再加总得到一个0到1之间的总匹配分。核心代码长这样def calculate_match_score(student, job): score 0.0 # 1. 学历匹配40% if student.education job.education_required: score 0.4 # 2. 技能匹配30% student_skills set(student.skills) job_skills set(job.skills) if student_skills and job_skills: intersection len(student_skills job_skills) union len(student_skills | job_skills) jaccard intersection / union if union else 0 score 0.3 * jaccard # 3. 地点匹配20% if student.expected_city job.location: score 0.2 # 4. 薪资匹配10% if job.salary_min student.expected_salary job.salary_max: score 0.1 return score技能匹配这里我用了Jaccard相似系数就是交集除以并集。用一个例子解释学生技能是[Python, Django, MySQL]职位要求的技能是[Python, Vue, MySQL]交集是Python和MySQL两个并集是Python、Django、MySQL、Vue四个相似度就是2除以4等于0.5。为什么这个方案适合毕设因为它有两大优势第一计算出来的分数完全可解释。答辩的时候老师问“你凭什么推荐这个职位给学生”你可以直接说“因为这个学生在学历、技能、地点、薪资四个方面加权匹配得分最高”甚至可以具体到“你们有2项技能重合地点一致”。这种可解释性在答辩时非常重要老师会认为你真的理解了业务逻辑。第二实现成本低、不需要训练数据纯Python代码加一个排序就可以完成。2.3 接口设计与权限控制前后端分离的项目接口设计直接决定联调效率。我当时把所有接口都设计成RESTful风格统一定义在根路由下然后分模块管理。这里给出我的核心接口清单方法路径功能权限POST/api/auth/register/注册公开POST/api/auth/token/登录获取JWT公开GET/api/student/profile/获取学生档案学生PUT/api/student/profile/更新学生档案学生GET/api/recommend/jobs/获取推荐职位列表学生GET/api/jobs/search/?keywordxx搜索职位学生POST/api/jobs/{id}/apply/投递简历学生GET/api/company/jobs/企业职位管理列表企业POST/api/company/jobs/发布职位企业GET/api/company/applications/查看收到的投递企业PUT/api/applications/{id}/status/更新投递状态企业权限控制是所有接口的重中之重。当时我用了Django REST Framework自带的permission_classes加自定义角色判断配合SimpleJWT做登录认证核心代码大致是这样from rest_framework.permissions import BasePermission class IsStudent(BasePermission): def has_permission(self, request, view): return request.user.is_authenticated and request.user.role student class IsCompany(BasePermission): def has_permission(self, request, view): return request.user.is_authenticated and request.user.role company然后每个视图函数声明权限from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated class RecommendJobView(APIView): permission_classes [IsAuthenticated, IsStudent] def get(self, request): # 推荐逻辑这套权限设计其实没有多难难的是你要想清楚学生可以访问哪些接口企业可以访问哪些接口哪些接口所有人都能访问。我建议动手写代码前先花二十分钟把接口权限矩阵画出来不然写到后面很容易出现学生调用企业接口的越权漏洞这种问题在答辩时一旦被老师发现印象分会大打折扣。3. 从零搭建前后端分离项目的完整实操3.1 环境搭建与项目初始化要点项目开发的第一步我建议把环境分成两层Python虚拟环境和Node前端环境。虚拟环境是新手最容易省略的一步但它能帮你避免依赖冲突的大坑。我当时使用Python 3.10Django版本选择了4.2 LTS搭配Django REST Framework和SimpleJWT。后端初始化主要这几步# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows下执行 venv/Scripts/activate # 安装核心依赖 pip install django4.2.* djangorestframework djangorestframework-simplejwt django-cors-headers mysqlclient # 创建项目与App django-admin startproject employment_system cd employment_system python manage.py startapp account python manage.py startapp job python manage.py startapp recommend python manage.py startapp companyApp的划分我按照模块职责来account管用户认证和学生/企业档案job管职位和投递company管企业端业务recommend管推荐计算。这样划分的好处是后期写代码时定位非常清晰不用在一个App里堆几千行代码。创建完App之后记得去settings.py里注册INSTALLED_APPS。数据库我用的是MySQL配置时注意编码创建数据库的时候一定要指定utf8mb4不然后面存中文会乱码CREATE DATABASE employment_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;3.2 跨域配置与JWT认证前后端联调的关键关卡前后端分离项目最典型的“拦路虎”就是跨域。前端跑在5173端口后端跑在8000端口浏览器默认会拦截跨域请求。我当时用django-cors-headers来解决这个问题配置非常简单# settings.py INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ] CORS_ALLOW_CREDENTIALS True注意CorsMiddleware要放在MIDDLEWARE列表尽量靠前的位置最好是放在最前面避免其他中间件已经处理了请求导致跨域头没有加上。JWT认证配置同样在settings.py里。我用的是djangorestframework-simplejwt核心配置如下from datetime import timedelta REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), } SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(hours2), REFRESH_TOKEN_LIFETIME: timedelta(days7), AUTH_HEADER_TYPES: (Bearer,), }这里我把访问token的有效期设成2小时。评论区有些经验帖建议设短一点但毕设项目中2小时足够太短的话前端要不停刷新token徒增工作量和演示时出错的概率。前端的处理逻辑是这样的登录成功后把access token存在localStorage然后写一个axios请求拦截器每次发请求自动带上token。再写一个响应拦截器当后端返回401时尝试用refresh token刷新刷新成功就重发原请求。这套逻辑在Vue项目里非常成熟代码量不大但很能体现专业性。3.3 推荐接口与投递流程的完整实现推荐接口是项目的核心出口。我强烈建议你把它单独写成一个视图逻辑走清晰些。具体流程我分了四步第一步从request.user取出关联的学生档案第二步从数据库里取出所有上架的职位is_activeTrue第三步遍历每一个职位调用calculate_match_score函数计算匹配分第四步按分数降序排序取前20个返回给前端。from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from job.models import JobPosition from job.serializers import JobPositionSerializer from .utils import calculate_match_score class RecommendJobView(APIView): permission_classes [IsAuthenticated] def get(self, request): student request.user.student_profile jobs JobPosition.objects.filter(is_activeTrue) scored_jobs [] for job in jobs: score calculate_match_score(student, job) if score 0: scored_jobs.append((score, job)) scored_jobs.sort(keylambda x: x[0], reverseTrue) top_jobs [job for score, job in scored_jobs[:20]] serializer JobPositionSerializer(top_jobs, manyTrue) return Response({ jobs: serializer.data, total: len(top_jobs) })投递流程也值得细心设计。因为要防止学生重复投递同一份工作我使用了Django的get_or_create方法class JobApplyView(APIView): permission_classes [IsAuthenticated] def post(self, request, job_id): student request.user.student_profile job get_object_or_404(JobPosition, idjob_id, is_activeTrue) application, created JobApplication.objects.get_or_create( studentstudent, jobjob, defaults{status: pending} ) if not created: return Response({detail: 你已经投递过该职位}, status400) return Response({detail: 投递成功}, status201)一个很小的细节但体现的是对业务逻辑的完整思考同样也是项目报告和答辩中很值得写的一笔。此外前端推荐页面的整体交互逻辑我用了Vue3的composition API来组织推荐列表页加载时调用后端推荐接口展示匹配分数最高的职位卡片学生点击“立即投递”按钮后调用投递接口如果返回400说明重复投递弹出提示。为了避免每次加载页面都重新全量请求我还简单做了一层数据缓存职位筛选条件不变的情况下直接使用本地数据。4. 常见问题与排查实录我踩过的坑4.1 环境与数据库新手最容易翻车的地方先说一个很多初学者都会遇到、但文档里不会写的问题JSONField在做数据库迁移时报错。这个错出现在Django的JSONField需要提供默认值如果你在模型里写了skills models.JSONField()而没有defaultlist执行makemigrations的时候Django会问你要不要设置默认值。如果你直接回车选了默认的空字符串后续所有调用skills的地方都会拿到一个空字符串而不是一个列表遍历的时候就崩了。正确的写法就是我在模型定义里写的那样必须带上defaultlist。再就是MySQL的版本兼容问题。如果你使用的是MySQL 8.0默认的认证插件是caching_sha2_password而Python的MySQLdb可能不认。我的解决方式是改用pymysql库然后在项目__init__.py里加上一行import pymysql pymysql.install_as_MySQLdb()顺便提一句数据库连接前建议把MySQL的字符集设置成utf8mb4避免丑到怀疑人生的中文乱码。4.2 跨域、认证与时间问题联调期的三大拦路虎跨域配置明明写了但还是请求失败。这种问题我用一句话总结先看浏览器控制台报错信息如果提示CORS error九成是CORS_ALLOWED_ORIGINS里的地址和前端实际请求地址不一致。比如前端访问的是http://localhost:5173你写的是http://127.0.0.1:5173对浏览器来说这完全是两个不同的源。JWT认证失效也是一个高频问题。我遇到过前端请求接口返回401但刷新token也没有用排查了半天最后发现是因为时间同步问题——服务器时间和本地时间相差太多JWT的iat签发时间校验失败。自测的时候注意一下设备时间是否正确。时区问题同样不容忽视。Django的settings.py里默认USE_TZTrue会导致DateTimeField在返回给前端时带上时区偏移量前端处理不好就会显示错误时间。我的处理方式是在序列化器里统一指定格式化方式这样返回的时间就是干净字符串不会有时区歧义。4.3 推荐模块的坑与改进思路第一坑推荐结果全为空。我用学生的技能[Python, Django]去匹配职位技能[Python, Django, Vue, React, MySQL]Jaccard相似度只有2/50.4如果权重设置得太低或者设置了最低阈值分数很容易掉到阈值以下结果一个都推不出来。解决办法有两个一是调低阈值甚至不设阈值只做排序二是把技能匹配从完全匹配改成包含匹配只要学生的技能和职位技能有任意一个交集就算命中。第二坑学历数值比较方向搞反。因为我把学历从高到低编码成1到4比较的时候应该写student.education job.education_required但我一开始写反了导致一个本科学生被推荐了博士要求的岗位闹出了笑话。这类逻辑错误单纯看代码很难发现建议自己在管理后台造几组测试数据提前验证匹配方向是否正确。第三坑性能问题。如果你的职位表有几千条数据每次请求都全表扫描一遍来计算匹配分速度会明显变慢。一个简单的优化思路是先做一个粗筛先把职位地点等于学生期望城市的职位筛出来再对这批职位计算精确分数。这个优化逻辑不仅提升了性能在答辩时还可以作为一个性能优化亮点来讲。5. 调试工具与测试数据让项目跑得更顺的一些小技巧调试和测试是很多同学的弱项但恰恰也是拉开差距的地方。我调试接口用的工具是Postman和浏览器开发者工具。Postman用来调后端接口很方便可以保存请求历史设置环境变量和Token。前端联调时我会打开开发者工具的Network面板观察请求头、响应体和状态码。如果某个接口返回500优先看Django控制台抛出的异常栈比瞎猜高效无数倍。测试数据这块我有自己的一套方法。我会在数据库里准备大约10个学生账号、10家企业和30个职位的数据岗位涵盖后端开发、前端开发、数据分析、产品经理等不同方向。每个学生的技能标签做差异化比如计算机专业学生擅长Python和Django设计专业学生擅长Photoshop和Figma。然后我会针对一个具体的学生验证推荐结果是否合理如果推荐出来的岗位确实和他的画像匹配就说明推荐逻辑工作正常。这组演示数据在后端演示和答辩前非常有用。6. 一点个人的项目复盘这套项目做完之后我有一个特别强烈的体会做毕设最重要的不是炫技而是把一条完整的主业务链路走通。从学生注册、完善画像到系统推荐职位再到学生投递、企业处理每一步都有数据流动每一步都有逻辑支撑。当你把这条链路完整打通你已经具备了一个后端开发真正需要的核心技能——理解需求、设计数据模型、实现业务逻辑、排查问题。最后再分享一个我自己的习惯与其等代码全部写完再写文档不如在开发过程中同步记录设计决策和踩坑记录。等你最后整理项目文档和答辩PPT的时候这些记录会让你省出大量时间内容也真实、有细节。祝你的毕业设计顺利过关这个题目认真做完出去面试的时候也有东西可聊。