☰
Django+Vue构建毕业生就业智能推荐系统:从数据建模到推荐算法落地
2026/10/12 6:10:23 网站建设 项目流程

选“基于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轻量灵活,适合小型项目很多功能要自己拼,开发效率不如Django
Node.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_length=20, choices=ROLE_CHOICES, default='student') phone = models.CharField(max_length=20, blank=True, null=True) class StudentProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='student_profile') school = models.CharField(max_length=100, verbose_name='学校') major = models.CharField(max_length=100, verbose_name='专业') education = models.IntegerField(choices=( (1, '专科'), (2, '本科'), (3, '硕士'), (4, '博士') ), default=2, verbose_name='学历层次') skills = models.JSONField(default=list, verbose_name='技能标签') expected_city = models.CharField(max_length=50, verbose_name='期望城市') expected_salary = models.IntegerField(default=0, verbose_name='期望薪资') resume = models.TextField(blank=True, verbose_name='简历简介') class JobPosition(models.Model): company = models.ForeignKey('company.CompanyProfile', on_delete=models.CASCADE, related_name='jobs') title = models.CharField(max_length=100, verbose_name='职位名称') industry = models.CharField(max_length=50, verbose_name='行业领域') location = models.CharField(max_length=50, verbose_name='工作地点') salary_min = models.IntegerField(default=0, verbose_name='薪资下限') salary_max = models.IntegerField(default=0, verbose_name='薪资上限') education_required = models.IntegerField(choices=( (1, '专科'), (2, '本科'), (3, '硕士'), (4, '博士') ), default=2, verbose_name='学历要求') skills = models.JSONField(default=list, verbose_name='技能标签') description = models.TextField(verbose_name='职位描述') is_active = models.BooleanField(default=True, verbose_name='是否上架') created_at = models.DateTimeField(auto_now_add=True)

这里有两个细节可以讲给答辩老师听。

第一个是学历字段的类型。学历我以前习惯用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/?keyword=xx搜索职位学生
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.10,Django版本选择了4.2 LTS,搭配Django REST Framework和SimpleJWT。

后端初始化主要这几步:

# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows下执行 venv/Scripts/activate # 安装核心依赖 pip install django==4.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 company

App的划分我按照模块职责来: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(hours=2), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'AUTH_HEADER_TYPES': ('Bearer',), }

这里我把访问token的有效期设成2小时。评论区有些经验帖建议设短一点,但毕设项目中2小时足够,太短的话前端要不停刷新token,徒增工作量和演示时出错的概率。

前端的处理逻辑是这样的:登录成功后把access token存在localStorage,然后写一个axios请求拦截器,每次发请求自动带上token。再写一个响应拦截器,当后端返回401时,尝试用refresh token刷新,刷新成功就重发原请求。这套逻辑在Vue项目里非常成熟,代码量不大但很能体现专业性。

3.3 推荐接口与投递流程的完整实现

推荐接口是项目的核心出口。我强烈建议你把它单独写成一个视图,逻辑走清晰些。

具体流程我分了四步:第一步从request.user取出关联的学生档案;第二步从数据库里取出所有上架的职位(is_active=True);第三步遍历每一个职位,调用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_active=True) scored_jobs = [] for job in jobs: score = calculate_match_score(student, job) if score > 0: scored_jobs.append((score, job)) scored_jobs.sort(key=lambda x: x[0], reverse=True) top_jobs = [job for score, job in scored_jobs[:20]] serializer = JobPositionSerializer(top_jobs, many=True) 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, id=job_id, is_active=True) application, created = JobApplication.objects.get_or_create( student=student, job=job, defaults={'status': 'pending'} ) if not created: return Response({'detail': '你已经投递过该职位'}, status=400) return Response({'detail': '投递成功'}, status=201)

一个很小的细节,但体现的是对业务逻辑的完整思考,同样也是项目报告和答辩中很值得写的一笔。

此外,前端推荐页面的整体交互逻辑我用了Vue3的composition API来组织:推荐列表页加载时调用后端推荐接口,展示匹配分数最高的职位卡片;学生点击“立即投递”按钮后调用投递接口,如果返回400说明重复投递,弹出提示。为了避免每次加载页面都重新全量请求,我还简单做了一层数据缓存,职位筛选条件不变的情况下直接使用本地数据。

4. 常见问题与排查实录:我踩过的坑

4.1 环境与数据库:新手最容易翻车的地方

先说一个很多初学者都会遇到、但文档里不会写的问题:JSONField在做数据库迁移时报错。这个错出现在Django的JSONField需要提供默认值,如果你在模型里写了skills = models.JSONField()而没有default=list,执行makemigrations的时候Django会问你要不要设置默认值。如果你直接回车选了默认的空字符串,后续所有调用skills的地方都会拿到一个空字符串而不是一个列表,遍历的时候就崩了。正确的写法就是我在模型定义里写的那样,必须带上default=list。

再就是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_TZ=True,会导致DateTimeField在返回给前端时带上时区偏移量,前端处理不好就会显示错误时间。我的处理方式是在序列化器里统一指定格式化方式,这样返回的时间就是干净字符串,不会有时区歧义。

4.3 推荐模块的坑与改进思路

第一坑:推荐结果全为空。我用学生的技能["Python", "Django"]去匹配职位技能["Python", "Django", "Vue", "React", "MySQL"],Jaccard相似度只有2/5=0.4,如果权重设置得太低或者设置了最低阈值,分数很容易掉到阈值以下,结果一个都推不出来。解决办法有两个:一是调低阈值甚至不设阈值,只做排序;二是把技能匹配从完全匹配改成包含匹配,只要学生的技能和职位技能有任意一个交集就算命中。

第二坑:学历数值比较方向搞反。因为我把学历从高到低编码成1到4,比较的时候应该写student.education >= job.education_required,但我一开始写反了,导致一个本科学生被推荐了博士要求的岗位,闹出了笑话。这类逻辑错误单纯看代码很难发现,建议自己在管理后台造几组测试数据,提前验证匹配方向是否正确。

第三坑:性能问题。如果你的职位表有几千条数据,每次请求都全表扫描一遍来计算匹配分,速度会明显变慢。一个简单的优化思路是先做一个粗筛:先把职位地点等于学生期望城市的职位筛出来,再对这批职位计算精确分数。这个优化逻辑不仅提升了性能,在答辩时还可以作为一个性能优化亮点来讲。

5. 调试工具与测试数据:让项目跑得更顺的一些小技巧

调试和测试是很多同学的弱项,但恰恰也是拉开差距的地方。我调试接口用的工具是Postman和浏览器开发者工具。Postman用来调后端接口很方便,可以保存请求历史,设置环境变量和Token。前端联调时我会打开开发者工具的Network面板,观察请求头、响应体和状态码。如果某个接口返回500,优先看Django控制台抛出的异常栈,比瞎猜高效无数倍。

测试数据这块,我有自己的一套方法。我会在数据库里准备大约10个学生账号、10家企业和30个职位的数据,岗位涵盖后端开发、前端开发、数据分析、产品经理等不同方向。每个学生的技能标签做差异化,比如计算机专业学生擅长Python和Django,设计专业学生擅长Photoshop和Figma。然后我会针对一个具体的学生验证推荐结果是否合理,如果推荐出来的岗位确实和他的画像匹配,就说明推荐逻辑工作正常。这组演示数据在后端演示和答辩前非常有用。

6. 一点个人的项目复盘

这套项目做完之后,我有一个特别强烈的体会:做毕设最重要的不是炫技,而是把一条完整的主业务链路走通。从学生注册、完善画像,到系统推荐职位,再到学生投递、企业处理,每一步都有数据流动,每一步都有逻辑支撑。当你把这条链路完整打通,你已经具备了一个后端开发真正需要的核心技能——理解需求、设计数据模型、实现业务逻辑、排查问题。

最后再分享一个我自己的习惯:与其等代码全部写完再写文档,不如在开发过程中同步记录设计决策和踩坑记录。等你最后整理项目文档和答辩PPT的时候,这些记录会让你省出大量时间,内容也真实、有细节。祝你的毕业设计顺利过关,这个题目认真做完,出去面试的时候也有东西可聊。

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

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

立即咨询