
简介基于PythonDjangoVueMySql开发的大学生就业招聘系统是一份面向计算机专业毕业设计或前后端分离实战学习的完整资源包。系统覆盖管理员、企业、求职者三类角色包含招聘信息管理、岗位申请、简历下载、在线留言、邀请面试等核心功能能够完整体现B/S架构项目的需求分析、系统设计与数据库设计流程。压缩包共759个文件约64.2MB主要包含py源码、vue前端组件、js/css静态资源、html页面、sql数据库脚本、mp4视频教程及bat安装运行脚本目录结构清晰便于直接部署与二次开发。系统中岗位分类、招聘信息、求职信息与邀请面试等模块关系清晰可基于源码继续扩展功能或用于答辩演示。视频教程与数据库脚本能够帮助使用者快速跑通项目并理解关键实现思路。已有177人学习下载适合需要快速搭建完整招聘类系统、准备毕业设计或提升DjangoVue开发能力的开发者。1. 一套毕业设计级招聘系统拆开看就是三个角色和四张表的博弈大学生就业招聘系统这类选题在毕业设计中经久不衰原因很简单业务闭环完整角色分明技术栈能同时覆盖 Django、Vue、MySQL 三个高频考核点。但拿到源码后真正把它跑起来、改明白的人并不多。这套系统最值得看的不是页面有多花哨而是它把管理员、企业、学生三类角色的权限边界和业务流招聘发布、岗位申请、简历投递、面试邀请落到了具体的数据表关系和 API 设计上。如果你是准备做前后端分离项目的在校生或者想快速复用一份带视频教程的完整工程做二次开发这篇拆解会直接告诉你从哪下手、哪些地方容易翻车。2. 后端部分Django 模型设计与 REST API 的权限隔离2.1 用户体系为什么不用 Django 默认的 User 表Django 自带的 auth.User 表能处理登录认证但招聘系统里天然存在三种身份管理员、企业用户、学生用户。直接用默认表会导致两个问题一是 profile 扩展表写得别扭二是权限判断时每次都要查关联表。常见做法是继承 AbstractUser 自定义用户模型加一个user_type字段做角色区分。这套系统的源码里走的也是这个路线核心模型大致长这样。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): # 角色区分1-管理员 2-企业用户 3-学生用户 USER_TYPE_CHOICES ( (1, admin), (2, company), (3, student), ) user_type models.SmallIntegerField(choicesUSER_TYPE_CHOICES, default3) phone models.CharField(max_length11, blankTrue, nullTrue) avatar models.ImageField(upload_toavatars/, blankTrue, nullTrue) class Meta: db_table recruit_useruser_type字段是整个权限体系的基石。后续所有视图层判断都用它做分支注册接口里通过前端传来的角色参数决定创建哪种用户。这里要注意一个坑AUTH_USER_MODEL配置必须在settings.py里指定否则迁移时会报冲突。# settings.py AUTH_USER_MODEL app_recruit.User2.2 核心业务模型的关联关系招聘系统的数据模型围绕五张核心表展开用户表、企业信息表、招聘信息表、岗位申请表、求职信息表。它们的关联关系是企业发布招聘信息学生对招聘信息发起申请同时学生可以维护自己的求职简历信息。class Company(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_namecompany) company_name models.CharField(max_length100) industry models.CharField(max_length50) # 行业分类 address models.CharField(max_length200) description models.TextField(blankTrue) class JobPost(models.Model): company models.ForeignKey(Company, on_deletemodels.CASCADE, related_namejob_posts) title models.CharField(max_length100) # 岗位名称 category models.CharField(max_length50) # 岗位分类 salary_min models.IntegerField(default0) salary_max models.IntegerField(default0) requirement models.TextField() # 任职要求 created_at models.DateTimeField(auto_now_addTrue) status models.BooleanField(defaultTrue) # 上架/下架 class JobApplication(models.Model): job models.ForeignKey(JobPost, on_deletemodels.CASCADE, related_nameapplications) student models.ForeignKey(User, on_deletemodels.CASCADE, related_nameapplications) resume_text models.TextField(blankTrue) # 申请时附带的求职说明 status models.SmallIntegerField(default0) # 0-待处理 1-已通过 2-已拒绝 apply_time models.DateTimeField(auto_now_addTrue) class Resume(models.Model): student models.OneToOneField(User, on_deletemodels.CASCADE, related_nameresume) education models.CharField(max_length100) # 学历 major models.CharField(max_length50) # 专业 experience models.TextField(blankTrue) # 项目/实习经历 expected_salary models.CharField(max_length50, blankTrue) updated_at models.DateTimeField(auto_nowTrue)模型设计上有一个值得留意的点JobApplication里直接冗余了resume_text字段而不是通过外键关联Resume表。好处是申请记录不随简历修改而失效坏处是简历更新后旧申请不会同步。对毕设场景来说这种取舍能少写很多联表查询逻辑。2.3 视图层与序列化器的权限控制前后端分离项目里Django 侧一般用djangorestframework提供 API。这套系统的 API 设计遵循了最基本的角色隔离原则企业只能操作自己的招聘信息学生只能操作自己的申请记录。下面是一段典型的招聘信息管理视图代码重点在于get_queryset里对身份的过滤。from rest_framework import viewsets, permissions from .models import JobPost from .serializers import JobPostSerializer class JobPostViewSet(viewsets.ModelViewSet): serializer_class JobPostSerializer permission_classes [permissions.IsAuthenticated] def get_queryset(self): user self.request.user # 管理员看全部企业只看自己发布的学生只看上架的 if user.user_type 1: return JobPost.objects.all() elif user.user_type 2: return JobPost.objects.filter(company__useruser) else: return JobPost.objects.filter(statusTrue) def perform_create(self, serializer): # 只有企业用户能发布招聘学生和管理员不允许 if self.request.user.user_type ! 2: raise PermissionError(只有企业用户才能发布招聘信息) company Company.objects.get(userself.request.user) serializer.save(companycompany)这里把权限判断放在get_queryset和perform_create两个位置分别解决「读」和「写」的隔离。很多初学者只重写get_queryset却忘记在创建时做身份校验导致学生能直接 POST 数据伪造企业招聘。权限从来不是加一个装饰器就完事而是要在数据出入口各设一道关卡。序列化器层面需要对JobPostSerializer做一层数据校验class JobPostSerializer(serializers.ModelSerializer): company_name serializers.CharField(sourcecompany.company_name, read_onlyTrue) class Meta: model JobPost fields [id, title, category, salary_min, salary_max, requirement, created_at, status, company_name] def validate(self, attrs): if attrs.get(salary_min, 0) attrs.get(salary_max, 0): raise serializers.ValidationError(最低薪资不能大于最高薪资) return attrssalary_min和salary_max的校验是实际开发中容易漏掉的逻辑。数据库层面可以不管但接口层面必须拦否则前端展示薪资区间时会出现倒挂。2.4 登录认证与 Token 机制前后端分离的登录认证通常用simplejwt来实现。Django 侧签发 JWTVue 侧存储 token每次请求在 header 里带上Authorization: Bearer token。# settings.py 关键配置 from datetime import timedelta SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(hours2), REFRESH_TOKEN_LIFETIME: timedelta(days7), AUTH_HEADER_TYPES: (Bearer,), }为什么用 JWT 而不是 Session核心原因是前后端分离后前端可能部署在独立域名或端口下Session 的 Cookie 跨域处理比较麻烦而 JWT 直接放在请求头里天然免疫 CSRF也方便前端做统一拦截。要注意 JWT 是无状态的服务端没法主动把 token 踢下线所以如果需要封禁用户得额外维护一个黑名单表毕设里通常不做但答辩时被问到要能答上来。3. 前端部分Vue 的角色路由与状态管理实现3.1 前端工程结构与核心依赖这套系统前端是基于 Vue 2 的从IndexAsideStatic.vue这类组件命名可以判断Vue 3 一般会用 Composition API 风格的组织方式。核心依赖包括vue-router、vuex、axios。项目结构里update-password.vue.bak这类备份文件说明开发过程中对部分组件做过重写拿到源码后可以先忽略.bak后缀文件它们不参与编译。路由配置是前后端分离项目里第一个要设计好的东西。招聘系统的路由必须按角色动态生成不能把所有页面全部静态注册。// router/index.js const routes [ { path: /login, component: Login, meta: { public: true } }, { path: /, component: Layout, redirect: /home, meta: { requiresAuth: true } }, ]; // 动态路由根据用户角色注册不同页面 export function buildRoutes(userType) { const roleRoutes { 1: [ // 管理员 { path: /user-manage, component: () import(/views/admin/UserManage.vue) }, { path: /company-manage, component: () import(/views/admin/CompanyManage.vue) }, { path: /category-manage, component: () import(/views/admin/CategoryManage.vue) }, ], 2: [ // 企业用户 { path: /job-publish, component: () import(/views/company/JobPublish.vue) }, { path: /application-list, component: () import(/views/company/ApplicationList.vue) }, { path: /resume-download, component: () import(/views/company/ResumeDownload.vue) }, ], 3: [ // 学生用户 { path: /job-search, component: () import(/views/student/JobSearch.vue) }, { path: /my-application, component: () import(/views/student/MyApplication.vue) }, { path: /resume-edit, component: () import(/views/student/ResumeEdit.vue) }, ] }; return roleRoutes[userType] || []; }这种按角色动态挂载路由的做法不只在菜单显示层面做控制而是从路由层面就让用户无法访问非授权页面。配合 Vue Router 的beforeEach钩子做登录态校验才算完整的权限链路。router.beforeEach((to, from, next) { const token localStorage.getItem(access_token); if (to.meta.public) { next(); } else if (!token) { next(/login); } else { next(); } });3.2 侧边栏菜单的角色适配拿到这套系统后IndexAsideStatic.vue就是左侧菜单栏组件。源码里静态写了管理员菜单但企业用户和学生用户的菜单要根据登录角色动态渲染。实现思路是在 Vuex 里存当前用户信息和user_type菜单组件遍历路由表生成。// store/modules/user.js const state { token: localStorage.getItem(access_token) || , userInfo: JSON.parse(localStorage.getItem(user_info) || {}), }; const mutations { SET_TOKEN(state, token) { state.token token; localStorage.setItem(access_token, token); }, SET_USER_INFO(state, info) { state.userInfo info; localStorage.setItem(user_info, JSON.stringify(info)); } }; const actions { async login({ commit }, payload) { const res await axios.post(/api/auth/login/, { username: payload.username, password: payload.password, user_type: payload.user_type }); commit(SET_TOKEN, res.data.access); commit(SET_USER_INFO, res.data.user); return res.data.user.user_type; } };菜单渲染时根据user_type过滤路由template el-menu :default-activeactiveMenu router el-menu-item v-foritem in visibleMenus :keyitem.path :indexitem.path {{ item.meta.title }} /el-menu-item /el-menu /template script export default { computed: { visibleMenus() { const userType this.$store.state.user.userInfo.user_type; return buildRoutes(userType); } } } /script这个做法的好处是前后端保持同一套路由语义后端接口校验身份前端路由控制视图双保险。坏处是前端打包后路由表是静态的刷新页面后要从 localStorage 恢复用户信息并重新生成菜单否则会出现刷新后菜单消失的问题。处理方式是加一个actions里的fetchUserInfo在 App.vue 的mounted里调用一次。3.3 招聘信息展示与分页加载学生端核心页面是招聘信息浏览。这里涉及前端调用后端接口的分页参数设计。Django REST Framework 的PageNumberPagination返回结构是{ count, next, previous, results }前端 axios 封装时需要统一处理。// api/job.js import request from /utils/request; export function getJobList(params) { return request({ url: /api/jobs/, method: get, params }); }// views/student/JobSearch.vue 核心逻辑 async loadJobs() { this.loading true; try { const res await getJobList({ page: this.currentPage, page_size: 10, keyword: this.keyword, category: this.category }); this.jobList res.data.results; this.total res.data.count; } finally { this.loading false; } }后端对应的分页器配置# settings.py REST_FRAMEWORK { DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 10, DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], }前端拿到count字段后传给分页组件el-pagination的total属性绑上去即可。注意这里page_size参数在 DRF 里默认只读配置的PAGE_SIZE如果要让前端自由控制每页数量需要在视图里自定义分页类或直接使用LimitOffsetPagination。毕设里一般固定 10 条/页就够不用过度设计。3.4 跨域与请求拦截的踩坑点前后端分离项目必然遇到跨域。后端 Django 侧需要装django-cors-headers# settings.py INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, http://127.0.0.1:8080, ]前端 axios 统一拦截器是另一个容易出错的地方。401 跳转登录页时要清理无效 token否则会带着过期 token 反复请求造成死循环。// utils/request.js axios.interceptors.response.use( response response, error { if (error.response error.response.status 401) { localStorage.removeItem(access_token); localStorage.removeItem(user_info); window.location.href /login; } return Promise.reject(error); } );这个拦截器是整个系统的安全兜底后端返回 401 即 token 过期或无效前端强制重新登录。如果不同步清理用户在页面间跳转时会反复弹出登录框或者白屏。4. 数据库脚本与岗位申请流程的完整数据链路4.1 建表脚本与初始化数据MySQL 侧的设计决定了这套系统能不能顺利跑起来。源码里自带的数据库脚本通常包含建库、建表、初始化管理员账号三个部分。拿到脚本后在 MySQL 8.x 下执行需要注意字符集问题。CREATE DATABASE IF NOT EXISTS recruit_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE recruit_db; -- 用户表 CREATE TABLE recruit_user ( id int NOT NULL AUTO_INCREMENT, username varchar(150) NOT NULL, password varchar(128) NOT NULL, user_type smallint NOT NULL DEFAULT 3, phone varchar(11) DEFAULT NULL, avatar varchar(100) DEFAULT NULL, is_active tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 招聘信息表 CREATE TABLE job_post ( id int NOT NULL AUTO_INCREMENT, company_id int NOT NULL, title varchar(100) NOT NULL, category varchar(50) NOT NULL, salary_min int DEFAULT 0, salary_max int DEFAULT 0, requirement text, status tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_company (company_id), KEY idx_category (category), CONSTRAINT fk_job_company FOREIGN KEY (company_id) REFERENCES recruit_company (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;utf8mb4不是可选项。如果你用的是 utf8 字符集企业发布招聘信息里一旦出现 emoji 表情整条写入直接报Incorrect string value错误。这是 MySQL 5.7 之后的硬性要求数据库脚本第一行就该把CHARACTER SET和COLLATE写死不依赖服务器默认配置。初始化管理员账号时密码必须是 Django 的加密格式不能直接明文插入。正确做法是通过createsuperuser命令生成或者用 Django shell 写入# 必须进入虚拟环境后执行 python manage.py shell -c from app_recruit.models import User User.objects.create_superuser(admin, admintest.com, admin123, user_type1) 这条命令同时解决两件事创建管理员用户、写入 Django 的 PBKDF2 密码哈希。直接 SQL 插入的明文密码在登录时永远验证不通过。4.2 岗位申请的完整数据流岗位申请是这套系统里最长的一条数据链路学生浏览招聘信息 → 点击申请 → 生成申请记录 → 企业查看申请列表 → 企业通过/拒绝 → 学生查看结果。每一步都有对应接口和页面。这条链路里最值得关注的是申请去重逻辑。一个学生不能对同一个岗位重复申请这不仅是业务规则更是索引设计问题。在job_application表上应该加联合唯一索引ALTER TABLE job_application ADD UNIQUE KEY uk_job_student (job_id, student_id);后端视图里也要配合捕获IntegrityError或者查询时预先判断。更稳妥的做法是在 Django 的perform_create中做业务层校验class JobApplicationViewSet(viewsets.ModelViewSet): serializer_class JobApplicationSerializer permission_classes [permissions.IsAuthenticated] def get_queryset(self): user self.request.user if user.user_type 2: # 企业看的是自己收到的申请 return JobApplication.objects.filter(job__company__useruser) return JobApplication.objects.filter(studentuser) def perform_create(self, serializer): student self.request.user job serializer.validated_data[job] # 业务层去重校验 if JobApplication.objects.filter(jobjob, studentstudent).exists(): raise serializers.ValidationError(您已申请过该岗位) serializer.save(studentstudent)注意get_queryset里企业用户的过滤逻辑企业只能看到自己发布岗位收到的申请不能看到其他企业的申请数据。数据隔离在这里体现得最直接查询条件必须经过job__company__useruser这一层跳转。4.3 企业与学生端的关键 SQL 查询场景企业端会用到两个高频查询查看某个岗位收到多少申请、按条件筛选简历。前端页面上看起来是表格自动刷新实际上后端可以通过 annotate 一次性把统计信息带上。# 企业端岗位申请统计 - 按岗位维度 from django.db.models import Count jobs JobPost.objects.filter(company__userrequest.user).annotate( app_countCount(applications), pending_countCount(applications, filterQ(applications__status0)) )生成的 SQL 类似SELECT job_post.id, job_post.title, COUNT(job_application.id) AS app_count, SUM(CASE WHEN job_application.status 0 THEN 1 ELSE 0 END) AS pending_count FROM job_post LEFT JOIN job_application ON job_post.id job_application.job_id WHERE job_post.company_id 1 GROUP BY job_post.id;学生端查看「我的申请」时需要连表带出岗位标题和企业名称my_applications JobApplication.objects.filter(studentrequest.user).select_related(job__company)select_related解决的是 N1 查询问题。如果在序列化器里通过嵌套关系读取公司名没有 select_related 的话每一条申请都会额外查询一次公司表申请列表多的时候接口响应会肉眼可见地变慢。这是 Django ORM 面试里必问的优化点也是写毕设时容易忽略的点。4.4 在线留言与消息通知的表设计在线留言功能在这套系统里是企业与学生的非结构化沟通渠道。留言表的常见设计是三字段核心发送者、接收者、内容。class Message(models.Model): sender models.ForeignKey(User, on_deletemodels.CASCADE, related_namesent_messages) receiver models.ForeignKey(User, on_deletemodels.CASCADE, related_namereceived_messages) content models.TextField() is_read models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue)企业发出面试邀请实际上就是一条带类型的留言。可以设计message_type字段区分「普通留言」和「面试邀请」面试邀请额外关联一条JobApplication记录这样学生点击邀请时可以直接跳转到对应岗位申请。5. 一键部署脚本的运作逻辑与工程化改造建议5.1 安装、运行、打包三个批处理脚本做了什么这套系统提供了1-install.bat、2-run.bat、3-build.bat三件套。这是从开发环境一键部署的角度做的自动化封装。逐个拆开看逻辑并不复杂:: 1-install.bat —— 安装依赖与初始化 echo off echo [Step 1/3] 创建虚拟环境... python -m venv venv call venv\Scripts\activate echo [Step 2/3] 安装 Python 依赖... pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple echo [Step 3/3] 初始化数据库... python manage.py makemigrations python manage.py migrate echo 安装完成请运行 2-run.bat 启动后端服务。 pause1-install.bat里用的是清华镜像源加速 pip 下载国内网络环境下实测比默认 PyPI 源快很多。makemigrations 和 migrate 分开执行是因为源码里可能存在已有迁移文件与新增模型不同步的情况分开执行能保证数据库同步时不会跳过遗漏。:: 2-run.bat —— 同时拉起后端与前端服务 echo off echo [Step 1/2] 启动 Django 后端 (http://127.0.0.1:8000)... start cmd /k call venv\Scripts\activate python manage.py runserver 0.0.0.0:8000 echo [Step 2/2] 启动 Vue 前端 (http://localhost:8080)... cd frontend start cmd /k npm run serve两个start cmd /k意味着会弹出两个新命令窗口分别跑服务互不阻塞。0.0.0.0:8000这个监听地址在开发环境够用但如果部署到云服务器上记得改端口和加上ALLOWED_HOSTS配置否则 Django 会拒绝请求。:: 3-build.bat —— 前端打包产物交给后端托管 echo off cd frontend call npm run build echo 打包完成dist 目录已生成。前端打包产物可以放到 Django 的静态目录下统一托管也可以独立部署到 Nginx。毕设答辩时通常只需要跑到第 2 步即可演示。5.2 数据库初始化失败时如何排查数据库相关报错是这套系统跑不起来的第一大原因。常见报错场景和对应处理方式如下报错现象可能原因排查指令django.db.utils.OperationalError: (2003, Cant connect to MySQL server...)MySQL 服务未启动或密码错误先确认net start mysql能启动服务再用 Navicat 或命令行mysql -u root -p测试凭据mysqlclient安装报错未装 VC 编译环境或版本冲突Python 3.8 以下用pip install mysqlclient2.1.13.10 建议用pymysql替代并在__init__.py里pymysql.install_as_MySQLdb()Table recruit_db.recruit_user doesnt exist数据表未迁移或数据库名配错检查settings.py里DATABASES的NAME执行python manage.py showmigrations查看迁移状态django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module缺少数据库驱动尝试pip install pymysql后在项目__init__.py中写入import pymysql; pymysql.install_as_MySQLdb()DATABASES配置模板如此DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: recruit_db, USER: root, PASSWORD: 123456, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, } } }5.3 前后端联调阶段的验证技巧项目跑起来之后先不要着急点页面。打开浏览器开发者工具按下面的顺序验证三条链路能最快定位问题第一登录接口验证。在 Network 面板里找到登录请求正常响应应返回access和refresh两个 token 字段以及用户角色信息。如果响应只有access而没有user信息说明后端的自定义登录视图没有返回完整数据前端 localStorage 里存的user_info会是 undefined菜单渲染直接白屏。第二带 token 的请求验证。点击任意需要认证的页面看请求头里有没有Authorization: Bearer token。如果缺失说明 axios 的请求拦截器没有正确配置。常见做法是在 request.js 里加axios.interceptors.request.use(config { const token localStorage.getItem(access_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });第三权限拦截验证。用学生账号手动输入管理员的 URL比如/user-manage。正确行为是后端返回 403 或 404而不是返回数据。如果学生账号能拿到用户列表数据说明后端视图的get_queryset过滤逻辑有问题需要回到第 2 章检查权限判断。5.4 答辩演示时的加分操作答辩场景下可以在演示完基础功能后加一个「角色切换对比」的环节同一个浏览器里开着管理员、企业、学生三个账号的页面依次切换展示同一个岗位在不同角色视角下的差异。这个对比能直观展示角色权限的设计合理性比纯讲代码更能拿到分数。另一个加分点是用django-debug-toolbar展示当前请求的 SQL 执行情况。安装它后在浏览器右侧能看到 Django 执行的所有 SQL 语句直接证明数据库设计合理、没有明显的 N1 查询问题。安装方式pip install django-debug-toolbar # settings.py 的 INSTALLED_APPS 增加 debug_toolbar # urls.py 增加 path(__debug__/, include(debug_toolbar.urls))这个工具装好后日常开发排查 API 响应慢也很有用——页面卡顿先看 SQL 面板而不是租地怀疑接口逻辑。本文还有配套的精品资源点击获取